diff --git a/package.json b/package.json index 4f5d9e83a..ea0f655c7 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "dependencies": { "@types/eventsource": "^1.1.2", "@types/node": ">= 8", + "@types/randombytes": "^2.0.0", "@types/urijs": "^1.19.2", "axios": "^0.19.0", "bignumber.js": "^4.0.0", @@ -138,6 +139,7 @@ "es6-promise": "^4.2.4", "eventsource": "^1.0.7", "lodash": "^4.17.11", + "randombytes": "^2.1.0", "stellar-base": "^1.0.3", "toml": "^2.3.0", "tslib": "^1.10.0", diff --git a/src/index.ts b/src/index.ts index ad07af319..ddd2f8688 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ export { SERVER_TIME_MAP, getCurrentServerTime, } from "./horizon_axios_client"; +export * from "./utils"; // expose classes and functions from stellar-base export * from "stellar-base"; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..194a37022 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,66 @@ +import randomBytes from "randombytes"; +import { + Account, + BASE_FEE, + Keypair, + Operation, + TransactionBuilder, +} from "stellar-base"; + +/** + * @namespace Utils + */ +export namespace Utils { + /** + * Returns a valid [SEP0010](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) + * challenge transaction which you can use for Stellar Web Authentication. + * + * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) + * @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} anchorName Anchor's name to be used in the manage_data key. + * @param {number} [timeout=300] Challenge duration (default to 5 minutes). + * @example + * import { Utils, Keypair, Network } from 'stellar-sdk' + * + * Network.useTestNetwork(); + * + * let serverKeyPair = Keypair.fromSecret("server-secret") + * let challenge = Utils.buildChallengeTx(serverKeyPair, "client-stellar-account-id", "SDF", 300) + * @returns {string} A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction. + */ + export function buildChallengeTx( + serverKeypair: Keypair, + clientAccountID: string, + anchorName: string, + timeout: number = 300, + ): string { + const account = new Account(serverKeypair.publicKey(), "-1"); + const now = Math.floor(Date.now() / 1000); + + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + timebounds: { + minTime: now, + maxTime: now + timeout, + }, + }) + .addOperation( + Operation.manageData({ + name: `${anchorName} auth`, + value: randomBytes(64), + source: clientAccountID, + }), + ) + .build(); + + transaction.sign(serverKeypair); + + return transaction + .toEnvelope() + .toXDR("base64") + .toString(); + } +} diff --git a/test/unit/utils_test.js b/test/unit/utils_test.js new file mode 100644 index 000000000..ef7357649 --- /dev/null +++ b/test/unit/utils_test.js @@ -0,0 +1,45 @@ +describe('Utils', function() { + describe('Utils.buildChallengeTx', function() { + it('returns challenge which follows SEP0010 spec', function() { + let keypair = StellarSdk.Keypair.random(); + + const challenge = StellarSdk.Utils.buildChallengeTx( + keypair, + "GBDIT5GUJ7R5BXO3GJHFXJ6AZ5UQK6MNOIDMPQUSMXLIHTUNR2Q5CFNF", + "SDF" + ); + + const transaction = new StellarSdk.Transaction(challenge); + + expect(transaction.sequence).to.eql("0"); + expect(transaction.source).to.eql(keypair.publicKey()); + expect(transaction.operations.length).to.eql(1); + + const { maxTime, minTime } = transaction.timeBounds; + + expect(parseInt(maxTime) - parseInt(minTime)).to.eql(300); + + const [ operation ] = transaction.operations; + + expect(operation.name).to.eql("SDF auth"); + expect(operation.source).to.eql("GBDIT5GUJ7R5BXO3GJHFXJ6AZ5UQK6MNOIDMPQUSMXLIHTUNR2Q5CFNF"); + expect(operation.type).to.eql("manageData"); + expect(operation.value.length).to.eql(64); + }); + + it('uses the passed-in timeout', function() { + let keypair = StellarSdk.Keypair.random(); + + const challenge = StellarSdk.Utils.buildChallengeTx( + keypair, + "GBDIT5GUJ7R5BXO3GJHFXJ6AZ5UQK6MNOIDMPQUSMXLIHTUNR2Q5CFNF", + "SDF", + 600 + ); + + const transaction = new StellarSdk.Transaction(challenge); + const { maxTime, minTime } = transaction.timeBounds; + expect(parseInt(maxTime) - parseInt(minTime)).to.eql(600); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 535f97a8e..aa871a823 100644 --- a/yarn.lock +++ b/yarn.lock @@ -132,6 +132,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.130.tgz#04b3a690d5f4fc34579963c99adae067b8c8eb5a" integrity sha512-H++wk0tbneBsRVfLkgAAd0IIpmpVr2Bj4T0HncoOsQf3/xrJexRYQK2Tqo0Ej3pFslM8GkMgdis9bu6xIb1ycw== +"@types/node@*": + version "12.6.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c" + integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg== + "@types/node@>= 8": version "11.13.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.8.tgz#e5d71173c95533be9842b2c798978f095f912aab" @@ -142,6 +147,13 @@ resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.0.tgz#ffb81cb023ff435a41d4710a29ab23c561dc9fdf" integrity sha512-bsTIJFVQv7jnvNiC42ld2pQW2KRI+pAG243L+iATvqzy3X6+NH1obz2itRKDZZ8VVhN3wjwYax/VBGCcXzgTqQ== +"@types/randombytes@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/randombytes/-/randombytes-2.0.0.tgz#0087ff5e60ae68023b9bc4398b406fea7ad18304" + integrity sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA== + dependencies: + "@types/node" "*" + "@types/urijs@^1.19.2": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.2.tgz#45212f53c3f14f1d90ca823a719b164748ad413b" @@ -6848,7 +6860,7 @@ randomatic@^3.0.0: kind-of "^6.0.0" math-random "^1.0.1" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==