From a3073e29ac9a14535296aa3467b952d41c6885f9 Mon Sep 17 00:00:00 2001 From: Jake Urban <10968980+JakeUrban@users.noreply.github.com> Date: Wed, 15 Sep 2021 14:36:31 -0700 Subject: [PATCH] SEP-10: muxed account & memo support (#709) Co-authored-by: George --- CHANGELOG.md | 9 +- package.json | 2 +- src/utils.ts | 71 ++++++++++---- test/unit/utils_test.js | 212 +++++++++++++++++++++++++++++++++++++--- yarn.lock | 8 +- 5 files changed, 265 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8fc4c462..37b3ce352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,18 @@ A breaking change will get clearly marked in this log. ## Unreleased +### Updates + +- Updates the following SEP-10 utility functions to be compilant with the protocols ([#709](https://github.com/stellar/js-stellar-sdk/pull/709/), [stellar-protocol/#1036](https://github.com/stellar/stellar-protocol/pull/1036)) + - Updated `utils.buildChallengeTx()` to accept muxed accounts (`M...`) for client account IDs + - Updated `utils.buildChallengeTx()` to accept a `memo` parameter to attach to the challenge transaction + - Updated `utils.readChallengeTx()` to provide a `memo` property in the returned object + - Updated `utils.readChallengeTx()` to validate challenge transactions with muxed accounts (`M...`) as the client account ID + ### Fix - Drops the `chai-http` dependency to be only for developers ([#707](https://github.com/stellar/js-stellar-sdk/pull/707)). - ## [v9.0.0-beta.0](https://github.com/stellar/js-stellar-sdk/compare/v8.2.5...v9.0.0-beta.0) This beta release adds **support for Automated Market Makers**. For details, you can refer to [CAP-38](https://stellar.org/protocol/cap-38) for XDR changes and [this document](https://docs.google.com/document/d/1pXL8kr1a2vfYSap9T67R-g72B_WWbaE1YsLMa04OgoU/view) for detailed changes to the Horizon API. diff --git a/package.json b/package.json index 2309d53ad..45e4ea9c5 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "eventsource": "^1.0.7", "lodash": "^4.17.21", "randombytes": "^2.1.0", - "stellar-base": "^6.0.1", + "stellar-base": "^6.0.3", "toml": "^2.3.0", "tslib": "^1.10.0", "urijs": "^1.19.1", diff --git a/src/utils.ts b/src/utils.ts index 86d5f221a..2019f7cbd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,9 @@ import { BASE_FEE, FeeBumpTransaction, Keypair, + Memo, + MemoID, + MemoNone, Operation, TimeoutInfinite, Transaction, @@ -25,11 +28,12 @@ export namespace Utils { * @function * @memberof Utils * @param {Keypair} serverKeypair Keypair for server's signing account. - * @param {string} clientAccountID The stellar account that the wallet wishes to authenticate with the server. + * @param {string} clientAccountID The stellar account (G...) or muxed account (M...) that the wallet wishes to authenticate with the server. * @param {string} homeDomain The fully qualified domain name of the service requiring authentication * @param {number} [timeout=300] Challenge duration (default to 5 minutes). * @param {string} networkPassphrase The network passphrase. If you pass this argument then timeout is required. * @param {string} webAuthDomain The fully qualified domain name of the service issuing the challenge. + * @param {string} [memo] The memo to attach to the challenge transaction. The memo must be of type `id`. If the `clientaccountID` is a muxed account, memos cannot be used. * @example * import { Utils, Keypair, Networks } from 'stellar-sdk' * @@ -44,11 +48,10 @@ export namespace Utils { timeout: number = 300, networkPassphrase: string, webAuthDomain: string, + memo: string | null = null, ): string { - if (clientAccountID.startsWith("M")) { - throw Error( - "Invalid clientAccountID: multiplexed accounts are not supported.", - ); + if (clientAccountID.startsWith("M") && memo) { + throw Error("memo cannot be used if clientAccountID is a muxed account"); } const account = new Account(serverKeypair.publicKey(), "-1"); @@ -61,7 +64,7 @@ export namespace Utils { // turned into binary represents 8 bits = 1 bytes. const value = randomBytes(48).toString("base64"); - const transaction = new TransactionBuilder(account, { + const builder = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase, timebounds: { @@ -74,6 +77,7 @@ export namespace Utils { name: `${homeDomain} auth`, value, source: clientAccountID, + withMuxing: true, }), ) .addOperation( @@ -82,9 +86,13 @@ export namespace Utils { value: webAuthDomain, source: account.accountId(), }), - ) - .build(); + ); + + if (memo) { + builder.addMemo(Memo.id(memo)); + } + const transaction = builder.build(); transaction.sign(serverKeypair); return transaction @@ -113,7 +121,7 @@ export namespace Utils { * @param {string} networkPassphrase The network passphrase, e.g.: 'Test SDF Network ; September 2015'. * @param {string|string[]} [homeDomains] The home domain that is expected to be included in the first Manage Data operation's string key. If an array is provided, one of the domain names in the array must match. * @param {string} webAuthDomain The home domain that is expected to be included as the value of the Manage Data operation with the 'web_auth_domain' key. If no such operation is included, this parameter is not used. - * @returns {Transaction|string|string} The actual transaction and the stellar public key (master key) used to sign the Manage Data operation, and matched home domain. + * @returns {Transaction|string|string|string} The actual transaction and the stellar public key (master key) used to sign the Manage Data operation, the matched home domain, and the memo attached to the transaction, which will be null if not present. */ export function readChallengeTx( challengeTx: string, @@ -121,19 +129,33 @@ export namespace Utils { networkPassphrase: string, homeDomains: string | string[], webAuthDomain: string, - ): { tx: Transaction; clientAccountID: string; matchedHomeDomain: string } { + ): { + tx: Transaction; + clientAccountID: string; + matchedHomeDomain: string; + memo: string | null; + } { if (serverAccountID.startsWith("M")) { throw Error( "Invalid serverAccountID: multiplexed accounts are not supported.", ); } - const transaction = TransactionBuilder.fromXDR( - challengeTx, - networkPassphrase, - ); - - if (!(transaction instanceof Transaction)) { + let transaction; + try { + transaction = new Transaction(challengeTx, networkPassphrase, true); + } catch { + try { + transaction = new FeeBumpTransaction( + challengeTx, + networkPassphrase, + true, + ); + } catch { + throw new InvalidSep10ChallengeError( + "Invalid challenge: unable to deserialize challengeTx transaction string", + ); + } throw new InvalidSep10ChallengeError( "Invalid challenge: expected a Transaction but received a FeeBumpTransaction", ); @@ -171,6 +193,21 @@ export namespace Utils { } const clientAccountID: string = operation.source!; + let memo: string | null = null; + if (transaction.memo.type !== MemoNone) { + if (clientAccountID.startsWith("M")) { + throw new InvalidSep10ChallengeError( + "The transaction has a memo but the client account ID is a muxed account", + ); + } + if (transaction.memo.type !== MemoID) { + throw new InvalidSep10ChallengeError( + "The transaction's memo must be of type `id`", + ); + } + memo = transaction.memo.value as string; + } + if (operation.type !== "manageData") { throw new InvalidSep10ChallengeError( "The transaction's operation type should be 'manageData'", @@ -272,7 +309,7 @@ export namespace Utils { ); } - return { tx: transaction, clientAccountID, matchedHomeDomain }; + return { tx: transaction, clientAccountID, matchedHomeDomain, memo }; } /** diff --git a/test/unit/utils_test.js b/test/unit/utils_test.js index ca9768b10..9dfda9f13 100644 --- a/test/unit/utils_test.js +++ b/test/unit/utils_test.js @@ -20,11 +20,12 @@ describe('Utils', function() { }); describe('Utils.buildChallengeTx', function() { - it('requires a non-muxed account', function() { + it('allows non-muxed accounts', function() { let keypair = StellarSdk.Keypair.random(); - + let muxedAddress = "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6"; + let challenge; expect(() => - StellarSdk.Utils.buildChallengeTx( + challenge = StellarSdk.Utils.buildChallengeTx( keypair, "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6", "testanchor.stellar.org", @@ -32,8 +33,63 @@ describe('Utils', function() { StellarSdk.Networks.TESTNET, "testanchor.stellar.org" ) + ).not.to.throw(); + const transaction = new StellarSdk.Transaction( + challenge, StellarSdk.Networks.TESTNET, true + ); + expect(transaction.operations[0].source).to.equal(muxedAddress); + }); + + it('allows ID memos', function() { + let keypair = StellarSdk.Keypair.random(); + let challenge; + expect(() => + challenge = StellarSdk.Utils.buildChallengeTx( + keypair, + StellarSdk.Keypair.random().publicKey(), + "testanchor.stellar.org", + 300, + StellarSdk.Networks.TESTNET, + "testanchor.stellar.org", + "8884404377665521220" + ) + ).not.to.throw(); + const transaction = new StellarSdk.Transaction( + challenge, StellarSdk.Networks.TESTNET, true + ); + expect(transaction.memo.value).to.equal("8884404377665521220"); + }); + + it('disallows non-ID memos', function() { + let keypair = StellarSdk.Keypair.random(); + expect(() => + challenge = StellarSdk.Utils.buildChallengeTx( + keypair, + StellarSdk.Keypair.random().publicKey(), + "testanchor.stellar.org", + 300, + StellarSdk.Networks.TESTNET, + "testanchor.stellar.org", + "memo text" + ) + ).to.throw(); + }); + + it('disallows memos with muxed accounts', function() { + let keypair = StellarSdk.Keypair.random(); + const muxedAddress = "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6"; + expect(() => + challenge = StellarSdk.Utils.buildChallengeTx( + keypair, + muxedAddress, + "testanchor.stellar.org", + 300, + StellarSdk.Networks.TESTNET, + "testanchor.stellar.org", + "8884404377665521220" + ) ).to.throw( - /Invalid clientAccountID: multiplexed accounts are not supported./ + /memo cannot be used if clientAccountID is a muxed account/ ); }); @@ -94,24 +150,28 @@ describe('Utils', function() { expect(maxTime).to.eql(600); expect(maxTime - minTime).to.eql(600); }); - }); - describe("Utils.readChallengeTx", function() { - it('requires a non-muxed account', function() { + it("throws an error if a muxed account and memo is passed", function () { + let keypair = StellarSdk.Keypair.random(); + const muxedAddress = "MCQQMHTBRF2NPCEJWO2JMDT2HBQ2FGDCYREY2YIBSHLTXDG54Y3KTWX3R7NBER62VBELC"; expect(() => - StellarSdk.Utils.readChallengeTx( - "avalidtx", - "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6", - "SDF", - 300, + StellarSdk.Utils.buildChallengeTx( + keypair, + muxedAddress, + "testanchor.stellar.org", + 600, StellarSdk.Networks.TESTNET, "testanchor.stellar.org", - "testanchor.stellar.org" + "10154623012567072189" ) ).to.throw( - /Invalid serverAccountID: multiplexed accounts are not supported./ + /memo cannot be used if clientAccountID is a muxed account/ ); }); + + }); + + describe("Utils.readChallengeTx", function() { it("requires a envelopeTypeTxV0 or envelopeTypeTx", function(){ let serverKP = StellarSdk.Keypair.random(); let clientKP = StellarSdk.Keypair.random(); @@ -213,9 +273,127 @@ describe('Utils', function() { tx: transaction, clientAccountID: clientKP.publicKey(), matchedHomeDomain: "SDF", + memo: null }); }); + it("returns the clientAccountID and memo if the challenge includes a memo", function() { + let serverKP = StellarSdk.Keypair.random(); + let clientKP = StellarSdk.Keypair.random(); + let clientMemo = "7659725268483412096"; + + const challenge = StellarSdk.Utils.buildChallengeTx( + serverKP, + clientKP.publicKey(), + "SDF", + 300, + StellarSdk.Networks.TESTNET, + "testanchor.stellar.org", + clientMemo + ); + + clock.tick(200); + + const transaction = new StellarSdk.Transaction( + challenge, + StellarSdk.Networks.TESTNET + ); + + expect( + StellarSdk.Utils.readChallengeTx( + challenge, + serverKP.publicKey(), + StellarSdk.Networks.TESTNET, + "SDF", + "testanchor.stellar.org" + ) + ).to.eql({ + tx: transaction, + clientAccountID: clientKP.publicKey(), + matchedHomeDomain: "SDF", + memo: clientMemo + }); + }); + + it("returns the muxed clientAccountID if included in the challenge", function() { + let serverKP = StellarSdk.Keypair.random(); + let muxedAddress = "MCQQMHTBRF2NPCEJWO2JMDT2HBQ2FGDCYREY2YIBSHLTXDG54Y3KTWX3R7NBER62VBELC"; + + const challenge = StellarSdk.Utils.buildChallengeTx( + serverKP, + muxedAddress, + "SDF", + 300, + StellarSdk.Networks.TESTNET, + "testanchor.stellar.org", + ); + + clock.tick(200); + + const transaction = new StellarSdk.Transaction(challenge, StellarSdk.Networks.TESTNET, true); + + expect( + StellarSdk.Utils.readChallengeTx( + challenge, + serverKP.publicKey(), + StellarSdk.Networks.TESTNET, + "SDF", + "testanchor.stellar.org" + ) + ).to.eql({ + tx: transaction, + clientAccountID: muxedAddress, + matchedHomeDomain: "SDF", + memo: null + }); + }); + + it("throws an error if the transaction uses a muxed account and has a memo", function () { + let serverKP = StellarSdk.Keypair.random(); + let clientKP = StellarSdk.Keypair.random(); + const serverAccount = new StellarSdk.Account(serverKP.publicKey(), "-1"); + const clientMuxedAddress = "MCQQMHTBRF2NPCEJWO2JMDT2HBQ2FGDCYREY2YIBSHLTXDG54Y3KTWX3R7NBER62VBELC"; + const transaction = new StellarSdk.TransactionBuilder( + serverAccount, + txBuilderOpts, + ) + .addOperation( + StellarSdk.Operation.manageData({ + source: clientMuxedAddress, + name: "testanchor.stellar.org auth", + value: randomBytes(48).toString("base64"), + withMuxing: true + }), + ) + .addMemo(new StellarSdk.Memo.id("5842698851377328257")) + .setTimeout(30) + .build(); + + transaction.sign(serverKP); + const challenge = transaction + .toEnvelope() + .toXDR("base64") + .toString(); + + const transactionRoundTripped = new StellarSdk.Transaction( + challenge, + StellarSdk.Networks.TESTNET + ); + + expect(() => + StellarSdk.Utils.readChallengeTx( + challenge, + serverKP.publicKey(), + StellarSdk.Networks.TESTNET, + "testanchor.stellar.org", + "testanchor.stellar.org" + ), + ).to.throw( + StellarSdk.InvalidSep10ChallengeError, + /The transaction has a memo but the client account ID is a muxed account/ + ) + }) + it("throws an error if the server hasn't signed the transaction", function () { let serverKP = StellarSdk.Keypair.random(); let clientKP = StellarSdk.Keypair.random(); @@ -647,6 +825,7 @@ describe('Utils', function() { tx: transactionRoundTripped, clientAccountID: clientKP.publicKey(), matchedHomeDomain: "testanchor.stellar.org", + memo: null }); }); @@ -691,6 +870,7 @@ describe('Utils', function() { tx: transactionRoundTripped, clientAccountID: clientKP.publicKey(), matchedHomeDomain: "testanchor.stellar.org", + memo: null }); }); @@ -893,6 +1073,7 @@ describe('Utils', function() { tx: transactionRoundTripped, clientAccountID: clientKP.publicKey(), matchedHomeDomain: "SDF", + memo: null }); }); @@ -1136,6 +1317,7 @@ describe('Utils', function() { tx: transactionRoundTripped, clientAccountID: clientKP.publicKey(), matchedHomeDomain: "testanchor.stellar.org", + memo: null }); }); @@ -1187,6 +1369,7 @@ describe('Utils', function() { tx: transactionRoundTripped, clientAccountID: clientKP.publicKey(), matchedHomeDomain: "testanchor.stellar.org", + memo: null }); }); @@ -1238,6 +1421,7 @@ describe('Utils', function() { tx: transactionRoundTripped, clientAccountID: clientKP.publicKey(), matchedHomeDomain: "testanchor.stellar.org", + memo: null }); }); }); diff --git a/yarn.lock b/yarn.lock index 2633c620c..c62946b11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7876,10 +7876,10 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stellar-base@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-6.0.1.tgz#5b960e7012348c79572be8d164713eb47aad07ed" - integrity sha512-YtlLAlnQ7gl6+N2BqsmH41IspxILFWywKgwF6jVynRPQZViGI/5oyEnZCUe9XhTH4Na2glqXpBxnDBcHsBLwWA== +stellar-base@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-6.0.3.tgz#14ac70ee74a974142fb2509b01649f9b3bb5cf3f" + integrity sha512-3xQo7VU2u84CQZ4ZxOk+TVXAUuMkwNbWzMcUSEcYja5i5CRX1RK1ivP9pn/VENIsLgu5tWhQeBMt3WHOo1ryBw== dependencies: base32.js "^0.1.0" bignumber.js "^4.0.0"