diff --git a/web3.js/package-lock.json b/web3.js/package-lock.json index 87711e96d..fb020ec5b 100644 --- a/web3.js/package-lock.json +++ b/web3.js/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "bn.js": "^5.0.0", + "borsh": "^0.4.0", "bs58": "^4.0.1", "buffer": "6.0.1", "buffer-layout": "^1.2.0", @@ -4682,6 +4683,25 @@ "node": ">=0.6" } }, + "node_modules/borsh": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz", + "integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==", + "dependencies": { + "@types/bn.js": "^4.11.5", + "bn.js": "^5.0.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/borsh/node_modules/@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -4799,6 +4819,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.2.0" @@ -17828,6 +17849,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", + "hasInstallScript": true, "dependencies": { "elliptic": "^6.5.2", "node-addon-api": "^2.0.0", @@ -19672,6 +19694,11 @@ "node": "*" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -20157,6 +20184,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.4.tgz", "integrity": "sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==", + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.2.0" @@ -24684,6 +24712,27 @@ } } }, + "borsh": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz", + "integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==", + "requires": { + "@types/bn.js": "^4.11.5", + "bn.js": "^5.0.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + }, + "dependencies": { + "@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "requires": { + "@types/node": "*" + } + } + } + }, "bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -36400,6 +36449,11 @@ } } }, + "text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", diff --git a/web3.js/package.json b/web3.js/package.json index caccecfdc..0fefc1921 100644 --- a/web3.js/package.json +++ b/web3.js/package.json @@ -64,6 +64,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "bn.js": "^5.0.0", + "borsh": "^0.4.0", "bs58": "^4.0.1", "buffer": "6.0.1", "buffer-layout": "^1.2.0", diff --git a/web3.js/rollup.config.js b/web3.js/rollup.config.js index 00b3ad45a..c281dfd7a 100644 --- a/web3.js/rollup.config.js +++ b/web3.js/rollup.config.js @@ -52,6 +52,7 @@ function generateConfig(configType, format) { config.external = [ /@babel\/runtime/, 'bn.js', + 'borsh', 'bs58', 'buffer-layout', 'crypto-hash', @@ -81,6 +82,7 @@ function generateConfig(configType, format) { config.external = [ /@babel\/runtime/, 'bn.js', + 'borsh', 'bs58', 'buffer', 'buffer-layout', diff --git a/web3.js/src/index.ts b/web3.js/src/index.ts index fa33259e5..aaed893ac 100644 --- a/web3.js/src/index.ts +++ b/web3.js/src/index.ts @@ -16,6 +16,7 @@ export * from './transaction'; export * from './validator-info'; export * from './vote-account'; export * from './sysvar'; +export * from './util/borsh-schema'; export * from './util/send-and-confirm-transaction'; export * from './util/send-and-confirm-raw-transaction'; export * from './util/cluster'; diff --git a/web3.js/src/publickey.ts b/web3.js/src/publickey.ts index 4837f545a..edbd6e516 100644 --- a/web3.js/src/publickey.ts +++ b/web3.js/src/publickey.ts @@ -4,6 +4,7 @@ import nacl from 'tweetnacl'; import {sha256} from 'crypto-hash'; import {Buffer} from 'buffer'; +import {Struct, SOLANA_SCHEMA} from './util/borsh-schema'; import {toBuffer} from './util/to-buffer'; /** @@ -11,10 +12,27 @@ import {toBuffer} from './util/to-buffer'; */ export const MAX_SEED_LENGTH = 32; +type PublicKeyInitData = + | number + | string + | Buffer + | Uint8Array + | Array + | PublicKeyData; + +type PublicKeyData = { + /** @internal */ + _bn: BN; +}; + +function isPublicKeyData(value: PublicKeyInitData): value is PublicKeyData { + return (value as PublicKeyData)._bn !== undefined; +} + /** * A public key */ -export class PublicKey { +export class PublicKey extends Struct { /** @internal */ _bn: BN; @@ -22,20 +40,25 @@ export class PublicKey { * Create a new PublicKey object * @param value ed25519 public key as buffer or base-58 encoded string */ - constructor(value: number | string | Buffer | Uint8Array | Array) { - if (typeof value === 'string') { - // assume base 58 encoding by default - const decoded = bs58.decode(value); - if (decoded.length != 32) { + constructor(value: PublicKeyInitData) { + super({}); + if (isPublicKeyData(value)) { + this._bn = value._bn; + } else { + if (typeof value === 'string') { + // assume base 58 encoding by default + const decoded = bs58.decode(value); + if (decoded.length != 32) { + throw new Error(`Invalid public key input`); + } + this._bn = new BN(decoded); + } else { + this._bn = new BN(value); + } + + if (this._bn.byteLength() > 32) { throw new Error(`Invalid public key input`); } - this._bn = new BN(decoded); - } else { - this._bn = new BN(value); - } - - if (this._bn.byteLength() > 32) { - throw new Error(`Invalid public key input`); } } @@ -167,6 +190,11 @@ export class PublicKey { } } +SOLANA_SCHEMA.set(PublicKey, { + kind: 'struct', + fields: [['_bn', 'u256']], +}); + // @ts-ignore let naclLowLevel = nacl.lowlevel; diff --git a/web3.js/src/util/borsh-schema.ts b/web3.js/src/util/borsh-schema.ts new file mode 100644 index 000000000..1c61fd1bc --- /dev/null +++ b/web3.js/src/util/borsh-schema.ts @@ -0,0 +1,34 @@ +import {Buffer} from 'buffer'; +import {serialize, deserialize} from 'borsh'; + +// Class wrapping a plain object +export class Struct { + constructor(properties: any) { + Object.assign(this, properties); + } + + encode(): Buffer { + return Buffer.from(serialize(SOLANA_SCHEMA, this)); + } + + static decode(data: Buffer): any { + return deserialize(SOLANA_SCHEMA, this, data); + } +} + +// Class representing a Rust-compatible enum, since enums are only strings or +// numbers in pure JS +export class Enum extends Struct { + enum: string = ''; + constructor(properties: any) { + super(properties); + if (Object.keys(properties).length !== 1) { + throw new Error('Enum can only take single value'); + } + Object.keys(properties).map(key => { + this.enum = key; + }); + } +} + +export const SOLANA_SCHEMA: Map = new Map(); diff --git a/web3.js/test/publickey.test.ts b/web3.js/test/publickey.test.ts index bc49bcf28..7d524f002 100644 --- a/web3.js/test/publickey.test.ts +++ b/web3.js/test/publickey.test.ts @@ -342,4 +342,11 @@ describe('PublicKey', function () { ); expect(PublicKey.isOnCurve(offCurve.toBuffer())).to.be.false; }); + + it('canBeSerializedWithBorsh', () => { + const publicKey = Keypair.generate().publicKey; + const encoded = publicKey.encode(); + const decoded = PublicKey.decode(encoded); + expect(decoded.equals(publicKey)).to.be.true; + }); });