Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add strkey parsing for contracts, and Address helper for building ScAddresses #572

Merged
merged 4 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stellar-base",
"version": "8.2.2-soroban.7",
"version": "8.2.2-soroban.9",
"description": "Low level stellar support library",
"main": "./lib/index.js",
"types": "./types/index.d.ts",
Expand Down
102 changes: 102 additions & 0 deletions src/address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { StrKey } from './strkey';
import xdr from './xdr';

/**
* Create a new Address object.
*
* `Address` represents a single address in the Stellar network. An address can
* represent an account or a contract.
*
* @constructor
*
* @param {string} address - ID of the account (ex.
* `GB3KJPLFUYN5VL6R3GU3EGCGVCKFDSD7BEDX42HWG5BWFKB3KQGJJRMA`). If you
* provide a muxed account address, this will throw; use {@link
* MuxedAccount} instead.
*/
export class Address {
constructor(address) {
if (StrKey.isValidEd25519PublicKey(address)) {
this._type = 'account';
this._key = StrKey.decodeEd25519PublicKey(address);
} else if (StrKey.isValidContract(address)) {
this._type = 'contract';
this._key = StrKey.decodeContract(address);
} else {
throw new Error('Unsupported address type');
}
}

/**
* Parses a string and returns an Address object.
*
* @param {string} address - The address to parse. ex. `GB3KJPLFUYN5VL6R3GU3EGCGVCKFDSD7BEDX42HWG5BWFKB3KQGJJRMA`
* @returns {Address}
*/
static fromString(address) {
return new Address(address);
}

/**
* Creates a new account Address object from a buffer of raw bytes.
*
* @param {Buffer} buffer - The bytes of an address to parse.
* @returns {Address}
*/
static account(buffer) {
return new Address(StrKey.encodeEd25519PublicKey(buffer));
}

/**
* Creates a new contract Address object from a buffer of raw bytes.
*
* @param {Buffer} buffer - The bytes of an address to parse.
* @returns {Address}
*/
static contract(buffer) {
return new Address(StrKey.encodeContract(buffer));
}

/**
* Serialize an address to string.
*
* @returns {string}
*/
toString() {
switch (this._type) {
case 'account':
return StrKey.encodeEd25519PublicKey(this._key);
case 'contract':
return StrKey.encodeContract(this._key);
default:
throw new Error('Unsupported address type');
}
}

/**
* Convert this Address to an xdr.ScVal type.
*
* @returns {xdr.ScVal}
*/
toScVal() {
return xdr.ScVal.scvObject(xdr.ScObject.scoAddress(this.toScAddress()));
}

/**
* Convert this Address to an xdr.ScAddress type.
*
* @returns {xdr.ScAddress}
*/
toScAddress() {
switch (this._type) {
case 'account':
return xdr.ScAddress.scAddressTypeAccount(
xdr.PublicKey.publicKeyTypeEd25519(this._key)
);
case 'contract':
return xdr.ScAddress.scAddressTypeContract(this._key);
default:
throw new Error('Unsupported address type');
}
}
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
} from './operation';
export * from './memo';
export { Account } from './account';
export * from './address';
export { Contract } from './contract';
export { MuxedAccount } from './muxed_account';
export { Claimant } from './claimant';
Expand Down
39 changes: 35 additions & 4 deletions src/strkey.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const versionBytes = {
med25519PublicKey: 12 << 3, // M
preAuthTx: 19 << 3, // T
sha256Hash: 23 << 3, // X
signedPayload: 15 << 3 // P
signedPayload: 15 << 3, // P
contract: 2 << 3 // C
};

const strkeyTypes = {
Expand All @@ -22,7 +23,8 @@ const strkeyTypes = {
M: 'med25519PublicKey',
T: 'preAuthTx',
X: 'sha256Hash',
P: 'signedPayload'
P: 'signedPayload',
C: 'contract'
};

/**
Expand Down Expand Up @@ -180,6 +182,33 @@ export class StrKey {
return isValid('signedPayload', address);
}

/**
* Encodes raw data to strkey contract (C...).
* @param {Buffer} data data to encode
* @returns {string}
*/
static encodeContract(data) {
return encodeCheck('contract', data);
}

/**
* Decodes strkey contract (C...) to raw data.
* @param {string} address address to decode
* @returns {Buffer}
*/
static decodeContract(address) {
return decodeCheck('contract', address);
}

/**
* Checks validity of alleged contract (C...) strkey address.
* @param {string} address signer key to check
* @returns {boolean}
*/
static isValidContract(address) {
return isValid('contract', address);
}

static getVersionByteForPrefix(address) {
return strkeyTypes[address[0]];
}
Expand Down Expand Up @@ -208,7 +237,8 @@ function isValid(versionByteName, encoded) {
case 'ed25519PublicKey': // falls through
case 'ed25519SecretSeed': // falls through
case 'preAuthTx': // falls through
case 'sha256Hash':
case 'sha256Hash': // falls through
case 'contract':
if (encoded.length !== 56) {
return false;
}
Expand Down Expand Up @@ -242,7 +272,8 @@ function isValid(versionByteName, encoded) {
case 'ed25519PublicKey': // falls through
case 'ed25519SecretSeed': // falls through
case 'preAuthTx': // falls through
case 'sha256Hash':
case 'sha256Hash': // falls through
case 'contract':
return decoded.length === 32;

case 'med25519PublicKey':
Expand Down
80 changes: 80 additions & 0 deletions test/unit/address_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
describe('Address', function() {
const ACCOUNT = 'GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB';
const CONTRACT = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE';
const MUXED_ADDRESS =
'MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK';

describe('.constructor', function() {
it('fails to create Address object from an invalid address', function() {
expect(() => new StellarBase.Address('GBBB')).to.throw(
/Unsupported address type/
);
});

it('creates an Address object for accounts', function() {
let account = new StellarBase.Address(ACCOUNT);
expect(account.toString()).to.equal(ACCOUNT);
});

it('creates an Address object for contracts', function() {
let account = new StellarBase.Address(CONTRACT);
expect(account.toString()).to.equal(CONTRACT);
});

it('wont create Address objects from muxed account strings', function() {
expect(() => {
new StellarBase.Account(MUXED_ADDRESS, '123');
}).to.throw(/MuxedAccount/);
});
});

describe('static constructors', function() {
it('.fromString', function() {
let account = StellarBase.Address.fromString(ACCOUNT);
expect(account.toString()).to.equal(ACCOUNT);
});

it('.account', function() {
let account = StellarBase.Address.account(Buffer.alloc(32));
expect(account.toString()).to.equal(
'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF'
);
});

it('.contract', function() {
let account = StellarBase.Address.contract(Buffer.alloc(32));
expect(account.toString()).to.equal(
'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4'
);
});
});

describe('.toScAddress', function() {
it('converts accounts to an ScAddress', function() {
const a = new StellarBase.Address(ACCOUNT);
const s = a.toScAddress();
expect(s).to.be.instanceof(StellarBase.xdr.ScAddress);
expect(s.switch()).to.equal(
StellarBase.xdr.ScAddressType.scAddressTypeAccount()
);
});

it('converts contracts to an ScAddress', function() {
const a = new StellarBase.Address(CONTRACT);
const s = a.toScAddress();
expect(s).to.be.instanceof(StellarBase.xdr.ScAddress);
expect(s.switch()).to.equal(
StellarBase.xdr.ScAddressType.scAddressTypeContract()
);
});
});

describe('.toScVal', function() {
it('converts to an ScAddress', function() {
const a = new StellarBase.Address(ACCOUNT);
const s = a.toScVal();
expect(s).to.be.instanceof(StellarBase.xdr.ScVal);
expect(s.obj().address()).to.deep.equal(a.toScAddress());
});
});
});
21 changes: 21 additions & 0 deletions test/unit/strkey_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,27 @@ describe('StrKey', function() {
});
});

describe('#contracts', function() {
it('valid w/ 32-byte payload', function() {
const strkey = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE';
const buf = StellarBase.StrKey.decodeContract(strkey);

expect(buf.toString('hex')).to.equal(
'363eaa3867841fbad0f4ed88c779e4fe66e56a2470dc98c0ec9c073d05c7b103'
);

expect(StellarBase.StrKey.encodeContract(buf)).to.equal(strkey);
});

it('isValid', function() {
const valid = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE';
expect(StellarBase.StrKey.isValidContract(valid)).to.be.true;
const invalid =
'GA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE';
expect(StellarBase.StrKey.isValidContract(invalid)).to.be.false;
});
});

describe('#invalidStrKeys', function() {
// From https://stellar.org/protocol/sep-23#invalid-test-cases
const BAD_STRKEYS = [
Expand Down
10 changes: 10 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export class Account {
incrementSequenceNumber(): void;
}

export class Address {
constructor(address: string);
static fromString(address: string): Address;
static account(buffer: Buffer): Address;
static contract(buffer: Buffer): Address;
toString(): string;
toScVal(): xdr.ScVal;
toScAddress(): xdr.ScAddress;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there an xdr.Address, too?

Copy link
Contributor Author

@paulbellamy paulbellamy Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lmao, no. There is a ContractAuth.AddressWithNonce, but that embeds an ScAddress


export class Contract {
constructor(contractId: string);
contractId(): string;
Expand Down