Skip to content

Commit

Permalink
Add conversion layer between xdr.SignerKeys and StrKeys (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shaptic authored Apr 6, 2022
1 parent e3e21b6 commit 852925d
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 57 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
92 changes: 92 additions & 0 deletions src/signerkey.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 14 additions & 2 deletions src/strkey.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -170,6 +179,10 @@ export class StrKey {
static isValidSignedPayload(address) {
return isValid('signedPayload', address);
}

static getVersionByteForPrefix(address) {
return strkeyTypes[address[0]];
}
}

/**
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 11 additions & 9 deletions src/transaction_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
*/
Expand All @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -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({
Expand Down Expand Up @@ -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)
);
}

Expand Down
62 changes: 62 additions & 0 deletions test/unit/signerkey_test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
49 changes: 6 additions & 43 deletions test/unit/strkey_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
});
});
});
Expand Down
5 changes: 4 additions & 1 deletion test/unit/transaction_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
});
Expand Down

0 comments on commit 852925d

Please sign in to comment.