From 852925d1a0508a6c22a97ff77c66ccb375e94f19 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 6 Apr 2022 10:30:14 -0700 Subject: [PATCH] Add conversion layer between `xdr.SignerKey`s and `StrKey`s (#520) --- CHANGELOG.md | 4 +- src/index.js | 1 + src/signerkey.js | 92 +++++++++++++++++++++++++++++++++++ src/strkey.js | 16 +++++- src/transaction.js | 2 +- src/transaction_builder.js | 20 ++++---- test/unit/signerkey_test.js | 62 +++++++++++++++++++++++ test/unit/strkey_test.js | 49 +++---------------- test/unit/transaction_test.js | 5 +- 9 files changed, 194 insertions(+), 57 deletions(-) create mode 100644 src/signerkey.js create mode 100644 test/unit/signerkey_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 178d97745..61cbf940b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ - Support for converting signed payloads ([CAP-40](https://stellar.org/protocol/cap-40)) to and from their StrKey (`P...`) representation ([#511](https://github.com/stellar/js-stellar-base/pull/511)). -- Support for creating transactions with the new ([CAP-21](https://stellar.org/protocol/cap-21)) preconditions via `TransactionBuilder`. ([#513](https://github.com/stellar/js-stellar-base/pull/513)). +- Support for creating transactions with the new preconditions ([CAP-21](https://stellar.org/protocol/cap-21)) via `TransactionBuilder` ([#513](https://github.com/stellar/js-stellar-base/pull/513)). + +- A way to convert between addresses (like `G...` and `P...`, i.e. the `StrKey` class) and their respective signer keys (i.e. `xdr.SignerKey`s), particularly for use in the new transaction preconditions ([#520](https://github.com/stellar/js-stellar-base/pull/520)). ### Fix diff --git a/src/index.js b/src/index.js index 68a376b98..b12675e59 100644 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,7 @@ export { MuxedAccount } from './muxed_account'; export { Claimant } from './claimant'; export { Networks } from './network'; export { StrKey } from './strkey'; +export { SignerKey } from './signerkey'; export { decodeAddressToMuxedAccount, encodeMuxedAccountToAddress, diff --git a/src/signerkey.js b/src/signerkey.js new file mode 100644 index 000000000..d3ec353ff --- /dev/null +++ b/src/signerkey.js @@ -0,0 +1,92 @@ +import xdr from './generated/stellar-xdr_generated'; +import { StrKey, encodeCheck, decodeCheck } from './strkey'; + +/** + * A container class with helpers to convert between signer keys + * (`xdr.SignerKey`) and {@link StrKey}s. + * + * It's primarly used for manipulating the `extraSigners` precondition on a + * {@link Transaction}. + * + * @see {@link TransactionBuilder.setExtraSigners} + */ +export class SignerKey { + /** + * Decodes a StrKey address into an xdr.SignerKey instance. + * + * Only ED25519 public keys (G...), pre-auth transactions (T...), hashes + * (H...), and signed payloads (P...) can be signer keys. + * + * @param {string} address a StrKey-encoded signer address + * @returns {xdr.SignerKey} + */ + static decodeAddress(address) { + const signerKeyMap = { + ed25519PublicKey: xdr.SignerKey.signerKeyTypeEd25519, + preAuthTx: xdr.SignerKey.signerKeyTypePreAuthTx, + sha256Hash: xdr.SignerKey.signerKeyTypeHashX, + signedPayload: xdr.SignerKey.signerKeyTypeEd25519SignedPayload + }; + + const vb = StrKey.getVersionByteForPrefix(address); + const encoder = signerKeyMap[vb]; + if (!encoder) { + throw new Error(`invalid signer key type (${vb})`); + } + + const raw = decodeCheck(vb, address); + switch (vb) { + case 'signedPayload': + return encoder( + new xdr.SignerKeyEd25519SignedPayload({ + ed25519: raw.slice(0, 32), + payload: raw.slice(32 + 4) + }) + ); + + case 'ed25519PublicKey': // falls through + case 'preAuthTx': // falls through + case 'sha256Hash': // falls through + default: + return encoder(raw); + } + } + + /** + * Encodes a signer key into its StrKey equivalent. + * + * @param {xdr.SignerKey} signerKey the signer + * @returns {string} the StrKey representation of the signer + */ + static encodeSignerKey(signerKey) { + let strkeyType; + let raw; + + switch (signerKey.switch()) { + case xdr.SignerKeyType.signerKeyTypeEd25519(): + strkeyType = 'ed25519PublicKey'; + raw = signerKey.value(); + break; + + case xdr.SignerKeyType.signerKeyTypePreAuthTx(): + strkeyType = 'preAuthTx'; + raw = signerKey.value(); + break; + + case xdr.SignerKeyType.signerKeyTypeHashX(): + strkeyType = 'sha256Hash'; + raw = signerKey.value(); + break; + + case xdr.SignerKeyType.signerKeyTypeEd25519SignedPayload(): + strkeyType = 'signedPayload'; + raw = signerKey.ed25519SignedPayload().toXDR('raw'); + break; + + default: + throw new Error(`invalid SignerKey (type: ${signerKey.switch()})`); + } + + return encodeCheck(strkeyType, raw); + } +} diff --git a/src/strkey.js b/src/strkey.js index a02f7be54..1077efb91 100644 --- a/src/strkey.js +++ b/src/strkey.js @@ -16,6 +16,15 @@ const versionBytes = { signedPayload: 15 << 3 // P }; +const strkeyTypes = { + G: 'ed25519PublicKey', + S: 'ed25519SecretSeed', + M: 'med25519PublicKey', + T: 'preAuthTx', + X: 'sha256Hash', + P: 'signedPayload' +}; + /** * StrKey is a helper class that allows encoding and decoding Stellar keys * to/from strings, i.e. between their binary (Buffer, xdr.PublicKey, etc.) and @@ -170,6 +179,10 @@ export class StrKey { static isValidSignedPayload(address) { return isValid('signedPayload', address); } + + static getVersionByteForPrefix(address) { + return strkeyTypes[address[0]]; + } } /** @@ -307,9 +320,8 @@ export function encodeCheck(versionByteName, data) { return base32.encode(unencoded); } +// Computes the CRC16-XModem checksum of `payload` in little-endian order function calculateChecksum(payload) { - // This code calculates CRC16-XModem checksum of payload - // and returns it as Buffer in little-endian order. const checksum = Buffer.alloc(2); checksum.writeUInt16LE(crc.crc16xmodem(payload), 0); return checksum; diff --git a/src/transaction.js b/src/transaction.js index 2150b2b0f..3d7401d32 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -189,7 +189,7 @@ export class Transaction extends TransactionBase { } /** - * @type {xdr.SignerKey[]} array of extra signers + * @type {string[]} array of extra signers (@{link StrKey}s) * @readonly */ get extraSigners() { diff --git a/src/transaction_builder.js b/src/transaction_builder.js index caa62cd00..0dcd4ddf3 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -8,6 +8,7 @@ import isArray from 'lodash/isArray'; import xdr from './generated/stellar-xdr_generated'; import { Transaction } from './transaction'; import { FeeBumpTransaction } from './fee_bump_transaction'; +import { SignerKey } from './signerkey'; import { Memo } from './memo'; import { decodeAddressToMuxedAccount } from './util/decode_encode_muxed_account'; @@ -103,7 +104,7 @@ export const TimeoutInfinite = 0; * seconds for the minimum account sequence age * @param {number} [opts.minAccountSequenceLedgerGap] - number of * ledgers for the minimum account sequence ledger gap - * @param {xdr.SignerKey[]} [opts.extraSigners] - list of the extra signers + * @param {string[]} [opts.extraSigners] - list of the extra signers * required for this transaction * @param {Memo} [opts.memo] - memo for the transaction * @param {string} [opts.networkPassphrase] passphrase of the @@ -400,7 +401,7 @@ export class TransactionBuilder { * by the sourceAccount or operations. Internally this will set the * `extraSigners` precondition. * - * @param {xdr.SignerKey[]} extraSigners required extra signers + * @param {string[]} extraSigners required extra signers (as {@link StrKey}s) * * @returns {TransactionBuilder} */ @@ -427,7 +428,9 @@ export class TransactionBuilder { /** * Set network nassphrase for the Transaction that will be built. * - * @param {string} [networkPassphrase] passphrase of the target stellar network (e.g. "Public Global Stellar Network ; September 2015"). + * @param {string} networkPassphrase passphrase of the target Stellar + * network (e.g. "Public Global Stellar Network ; September 2015"). + * * @returns {TransactionBuilder} */ setNetworkPassphrase(networkPassphrase) { @@ -483,8 +486,7 @@ export class TransactionBuilder { ledgerBounds = new xdr.LedgerBounds(this.ledgerbounds); } - let minSeqNum = - this.minAccountSequence !== null ? this.minAccountSequence : '0'; + let minSeqNum = this.minAccountSequence || '0'; minSeqNum = new xdr.SequenceNumber(UnsignedHyper.fromString(minSeqNum)); const minSeqAge = UnsignedHyper.fromString( @@ -495,10 +497,10 @@ export class TransactionBuilder { const minSeqLedgerGap = this.minAccountSequenceLedgerGap || 0; - // TODO: Input here should be some StrKey abstraction, then we convert it - // to xdr.SignerKey here. const extraSigners = - this.extraSigners !== null ? clone(this.extraSigners) : []; + this.extraSigners !== null + ? this.extraSigners.map(SignerKey.decodeAddress) + : []; attrs.cond = xdr.Preconditions.precondV2( new xdr.PreconditionsV2({ @@ -536,7 +538,7 @@ export class TransactionBuilder { this.minAccountSequence !== null || this.minAccountSequenceAge !== null || this.minAccountSequenceLedgerGap !== null || - this.extraSigners !== null + (this.extraSigners !== null && this.extraSigners.length > 0) ); } diff --git a/test/unit/signerkey_test.js b/test/unit/signerkey_test.js new file mode 100644 index 000000000..a9d45e9c3 --- /dev/null +++ b/test/unit/signerkey_test.js @@ -0,0 +1,62 @@ +describe('SignerKey', function() { + describe('encode/decode roundtrip', function() { + const TEST_CASES = [ + { + strkey: 'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ', + type: StellarBase.xdr.SignerKeyType.signerKeyTypeEd25519() + }, + { + strkey: 'TBU2RRGLXH3E5CQHTD3ODLDF2BWDCYUSSBLLZ5GNW7JXHDIYKXZWHXL7', + type: StellarBase.xdr.SignerKeyType.signerKeyTypePreAuthTx() + }, + { + strkey: 'XBU2RRGLXH3E5CQHTD3ODLDF2BWDCYUSSBLLZ5GNW7JXHDIYKXZWGTOG', + type: StellarBase.xdr.SignerKeyType.signerKeyTypeHashX() + }, + { + strkey: + 'PA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAQACAQDAQCQMBYIBEFAWDANBYHRAEISCMKBKFQXDAMRUGY4DUPB6IBZGM', + type: StellarBase.xdr.SignerKeyType.signerKeyTypeEd25519SignedPayload() + } + ]; + + TEST_CASES.forEach((testCase) => { + it(`works for ${testCase.strkey.substring(0, 5)}...`, function() { + const skey = StellarBase.SignerKey.decodeAddress(testCase.strkey); + expect(skey.switch()).to.eql(testCase.type); + + const rawXdr = skey.toXDR('raw'); + const rawSk = StellarBase.xdr.SignerKey.fromXDR(rawXdr, 'raw'); + expect(rawSk).to.eql(skey); + + const address = StellarBase.SignerKey.encodeSignerKey(skey); + expect(address).to.equal(testCase.strkey); + }); + }); + }); + + describe('error cases', function() { + [ + // these are valid strkeys, just not valid signers + 'SAB5556L5AN5KSR5WF7UOEFDCIODEWEO7H2UR4S5R62DFTQOGLKOVZDY', + 'MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK', + // this is (literal) nonsense + 'NONSENSE' + ].forEach((strkey) => { + it(`fails on ${strkey.substring(0, 5)}...`, function() { + expect(() => { + StellarBase.SignerKey.decodeAddress(strkey); + }).to.throw(/invalid signer key type/i); + }); + }); + + it('fails on invalid strkey', function() { + expect(() => + // address taken from strkey_test.js#invalidStrKeys + StellarBase.SignerKey.decodeAddress( + 'G47QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVP2I' + ) + ).to.throw(/invalid version byte/i); + }); + }); +}); diff --git a/test/unit/strkey_test.js b/test/unit/strkey_test.js index b3c2ce4bd..95decce2c 100644 --- a/test/unit/strkey_test.js +++ b/test/unit/strkey_test.js @@ -230,48 +230,10 @@ describe('StrKey', function() { const PUBKEY = 'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ'; const MPUBKEY = 'MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK'; - const RAW_MPUBKEY = Buffer.from([ - 0x3f, - 0x0c, - 0x34, - 0xbf, - 0x93, - 0xad, - 0x0d, - 0x99, - 0x71, - 0xd0, - 0x4c, - 0xcc, - 0x90, - 0xf7, - 0x05, - 0x51, - 0x1c, - 0x83, - 0x8a, - 0xad, - 0x97, - 0x34, - 0xa4, - 0xa2, - 0xfb, - 0x0d, - 0x7a, - 0x03, - 0xfc, - 0x7f, - 0xe8, - 0x9a, - 0x80, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00 - ]); + const RAW_MPUBKEY = Buffer.from( + '3f0c34bf93ad0d9971d04ccc90f705511c838aad9734a4a2fb0d7a03fc7fe89a8000000000000000', + 'hex' + ); describe('#muxedAccounts', function() { it('encodes & decodes M... addresses correctly', function() { @@ -445,7 +407,8 @@ describe('StrKey', function() { BAD_STRKEYS.forEach((address) => { it(`fails in expected case ${address}`, function() { - expect(() => StellarBase.StrKey.decodeCheck(address[0])).to.throw(); + const vb = StellarBase.StrKey.getVersionByteForPrefix(address); + expect(() => StellarBase.StrKey.decodeCheck(vb, address)).to.throw(); }); }); }); diff --git a/test/unit/transaction_test.js b/test/unit/transaction_test.js index 8f34ffe09..5d70ca222 100644 --- a/test/unit/transaction_test.js +++ b/test/unit/transaction_test.js @@ -711,7 +711,10 @@ describe('Transaction', function() { .setTimeout(5) .setExtraSigners([address]) .build(); - expect(tx.extraSigners).to.eql([address]); + expect(tx.extraSigners).to.have.lengthOf(1); + expect( + tx.extraSigners.map(StellarBase.SignerKey.encodeSignerKey) + ).to.eql([address]); }); }); });