From c44d76a266a640bff631ca7547b7779505430e07 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Mon, 20 May 2024 13:42:59 -0400 Subject: [PATCH] Release/1.5.0 (#130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump version to 1.5.0 * [Soroban] Add getTokenInvocationArgs function in new workspace (#119) * [Soroban] Add token parsing/formatting functions (#120) * [Soroban] Add scValByType function (#121) * [Soroban] Add GH Action and README (#122) * add tests for moneygram transactions (#123) * Use relative path to access "walletSdk" root folder (#125) * add AuthHeaderSigner (#124) * add AuthHeaderSigner * cmments * fix test * print logs * fix * Cleanup * Update @stellar/typescript-wallet-sdk/src/walletSdk/Auth/AuthHeaderSigner.ts Co-authored-by: Cássio Marcos Goulart <3228151+CassioMG@users.noreply.github.com> * snake case * add test for checking against example JWT --------- Co-authored-by: Cássio Marcos Goulart <3228151+CassioMG@users.noreply.github.com> * Add e2e test for testing browser build (#126) * add challengeToken to km header (#127) * add challengeToken to km header * move to dev deps * fix test * [Soroban] Add generic getInvocationDetails helper (#128) * updates BrowserStorageConfigParams method types (#129) * add changelogs, and update package versions to all be in align (#131) * update integration tests readme (#132) * add defaultauthheadersigner test with anchorplatform (#133) * add beta build github action (#135) * add canary build github action * add contributing guide with releasing directions * use timestamp * add top level readme (#136) * add top level readme * update * update * update * update * yarn install * example code * update changelogs (#137) * change to param (#138) --------- Co-authored-by: Alec Charbonneau Co-authored-by: Cássio Marcos Goulart <3228151+CassioMG@users.noreply.github.com> Co-authored-by: aristides --- .eslintrc.js | 2 + .github/workflows/npmPublishBeta.yml | 38 ++ .github/workflows/npmPublishSdkSoroban.yml | 22 + .github/workflows/playwrightTests.yml | 16 + .../typescript-wallet-sdk-km/CHANGELOG.MD | 13 + .../typescript-wallet-sdk-km/package.json | 3 +- .../src/Plugins/BrowserStorageFacade.ts | 6 +- .../src/Types/index.ts | 1 + .../src/keyManager.ts | 8 +- .../test/keyManager.test.ts | 108 ++++ .../CHANGELOG.MD | 8 + .../typescript-wallet-sdk-soroban/README.md | 80 +++ .../babel.config.js | 5 + .../package.json | 41 ++ .../src/Helpers/formatTokenAmount.ts | 23 + .../src/Helpers/getInvocationDetails.ts | 81 +++ .../src/Helpers/getTokenInvocationArgs.ts | 94 +++ .../src/Helpers/index.ts | 5 + .../src/Helpers/parseTokenAmount.ts | 33 ++ .../src/Helpers/scValByType.ts | 97 +++ .../src/Types/index.ts | 39 ++ .../src/index.ts | 2 + .../test/helpers.test.ts | 555 ++++++++++++++++++ .../test/tsconfig.json | 10 + .../test/utils/index.ts | 38 ++ .../tsconfig.json | 10 + .../webpack.config.js | 51 ++ @stellar/typescript-wallet-sdk/CHANGELOG.MD | 88 +++ .../examples/sep24/sep24.ts | 13 +- .../typescript-wallet-sdk/jest.e2e.config.js | 9 + @stellar/typescript-wallet-sdk/package.json | 7 +- @stellar/typescript-wallet-sdk/src/index.ts | 5 + .../src/walletSdk/Anchor/index.ts | 2 +- .../src/walletSdk/Auth/AuthHeaderSigner.ts | 162 +++++ .../src/walletSdk/Auth/index.ts | 50 +- .../src/walletSdk/Exceptions/index.ts | 19 + .../src/walletSdk/Horizon/AccountService.ts | 2 +- .../src/walletSdk/Horizon/Stellar.ts | 2 +- .../Horizon/Transaction/TransactionBuilder.ts | 2 +- .../src/walletSdk/Recovery/AccountRecover.ts | 2 +- .../src/walletSdk/Recovery/index.ts | 4 +- .../src/walletSdk/Types/auth.ts | 19 + .../src/walletSdk/Types/horizon.ts | 2 +- .../src/walletSdk/Types/index.ts | 2 +- .../src/walletSdk/Types/recovery.ts | 4 +- @stellar/typescript-wallet-sdk/test/README.md | 18 - .../typescript-wallet-sdk/test/e2e/README.md | 11 + .../test/e2e/browser.test.ts | 52 ++ .../test/integration/README.md | 38 ++ .../test/integration/anchorplatform.test.ts | 9 + .../typescript-wallet-sdk/test/server.test.ts | 13 + .../typescript-wallet-sdk/test/wallet.test.ts | 85 +++ .../typescript-wallet-sdk/webpack.config.js | 4 + CONTRIBUTING.md | 38 ++ README.md | 52 ++ jest.config.js | 8 +- package.json | 6 +- yarn.lock | 21 +- 58 files changed, 2089 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/npmPublishBeta.yml create mode 100644 .github/workflows/npmPublishSdkSoroban.yml create mode 100644 .github/workflows/playwrightTests.yml create mode 100644 @stellar/typescript-wallet-sdk-km/CHANGELOG.MD create mode 100644 @stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD create mode 100644 @stellar/typescript-wallet-sdk-soroban/README.md create mode 100644 @stellar/typescript-wallet-sdk-soroban/babel.config.js create mode 100644 @stellar/typescript-wallet-sdk-soroban/package.json create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/Helpers/formatTokenAmount.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/Helpers/getInvocationDetails.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/Helpers/getTokenInvocationArgs.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/Helpers/index.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/Helpers/parseTokenAmount.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/Types/index.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/src/index.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/test/tsconfig.json create mode 100644 @stellar/typescript-wallet-sdk-soroban/test/utils/index.ts create mode 100644 @stellar/typescript-wallet-sdk-soroban/tsconfig.json create mode 100644 @stellar/typescript-wallet-sdk-soroban/webpack.config.js create mode 100644 @stellar/typescript-wallet-sdk/CHANGELOG.MD create mode 100644 @stellar/typescript-wallet-sdk/jest.e2e.config.js create mode 100644 @stellar/typescript-wallet-sdk/src/walletSdk/Auth/AuthHeaderSigner.ts delete mode 100644 @stellar/typescript-wallet-sdk/test/README.md create mode 100644 @stellar/typescript-wallet-sdk/test/e2e/README.md create mode 100644 @stellar/typescript-wallet-sdk/test/e2e/browser.test.ts create mode 100644 @stellar/typescript-wallet-sdk/test/integration/README.md create mode 100644 CONTRIBUTING.md create mode 100644 README.md diff --git a/.eslintrc.js b/.eslintrc.js index 9c9b10b..494cf7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,8 @@ module.exports = { "@stellar/typescript-wallet-sdk/test/tsconfig.json", "@stellar/typescript-wallet-sdk-km/tsconfig.json", "@stellar/typescript-wallet-sdk-km/test/tsconfig.json", + "@stellar/typescript-wallet-sdk-soroban/tsconfig.json", + "@stellar/typescript-wallet-sdk-soroban/test/tsconfig.json", ], sourceType: "module", }, diff --git a/.github/workflows/npmPublishBeta.yml b/.github/workflows/npmPublishBeta.yml new file mode 100644 index 0000000..bd032e9 --- /dev/null +++ b/.github/workflows/npmPublishBeta.yml @@ -0,0 +1,38 @@ +name: typescript-wallet-sdk beta build +on: + push: + branches: + - develop +jobs: + npm-beta: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 18 + + - run: yarn install + - run: yarn build + - run: yarn test:ci + + - name: Create beta package version + run: | + timestamp=$(date +%s%3N) + current_version=$(jq -r '.version' @stellar/typescript-wallet-sdk/package.json) + echo "new_version=${current_version}-beta.${timestamp}" >> $GITHUB_ENV + + - name: Update package.json version + uses: jossef/action-set-json-field@6e6d7e639f24b3955ef682815317b5613ac6ca12 # v1 + with: + file: ./@stellar/typescript-wallet-sdk/package.json + field: version + value: ${{ env.new_version }} + + - name: Publish beta build + run: | + cd @stellar/typescript-wallet-sdk + yarn publish --tag beta --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/npmPublishSdkSoroban.yml b/.github/workflows/npmPublishSdkSoroban.yml new file mode 100644 index 0000000..7f97ffb --- /dev/null +++ b/.github/workflows/npmPublishSdkSoroban.yml @@ -0,0 +1,22 @@ +name: npm publish wallet sdk Soroban +on: [workflow_dispatch] +jobs: + npm-publish: + name: npm-publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 18 + registry-url: https://registry.npmjs.org/ + - run: yarn install + - run: yarn build + - run: yarn test:ci + + - name: Publish to NPM + run: | + cd @stellar/typescript-wallet-sdk-soroban + yarn publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/playwrightTests.yml b/.github/workflows/playwrightTests.yml new file mode 100644 index 0000000..97898b9 --- /dev/null +++ b/.github/workflows/playwrightTests.yml @@ -0,0 +1,16 @@ +name: Playwright Tests +on: [pull_request] +jobs: + playwright: + name: "Playwright e2e Tests" + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.43.0-jammy + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 18 + - run: yarn install + - run: yarn build + - run: yarn test:e2e:ci diff --git a/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD b/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD new file mode 100644 index 0000000..af756a4 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD @@ -0,0 +1,13 @@ +# Release notes - Typescript Wallet SDK Key Manager - 1.5.0 + +### Added +* Challenge token to param to auth header + +### Fixed +* Update BrowserStorageConfigParams method types + +# Release notes - Typescript Wallet SDK Key Manager - 1.4.0 + +### Added +* Init to the project, added key manager functionality + diff --git a/@stellar/typescript-wallet-sdk-km/package.json b/@stellar/typescript-wallet-sdk-km/package.json index c75a6d6..4ca7424 100644 --- a/@stellar/typescript-wallet-sdk-km/package.json +++ b/@stellar/typescript-wallet-sdk-km/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk-km", - "version": "1.0.1", + "version": "1.5.0", "engines": { "node": ">=18" }, @@ -16,6 +16,7 @@ "@stellar/tsconfig": "^1.0.2", "@types/jest": "^29.5.12", "@typescript-eslint/parser": "^7.1.1", + "@stellar/typescript-wallet-sdk": "*", "babel-jest": "^29.7.0", "husky": "^9.0.11", "jest": "^29.7.0", diff --git a/@stellar/typescript-wallet-sdk-km/src/Plugins/BrowserStorageFacade.ts b/@stellar/typescript-wallet-sdk-km/src/Plugins/BrowserStorageFacade.ts index 8a0e428..783481b 100644 --- a/@stellar/typescript-wallet-sdk-km/src/Plugins/BrowserStorageFacade.ts +++ b/@stellar/typescript-wallet-sdk-km/src/Plugins/BrowserStorageFacade.ts @@ -3,9 +3,11 @@ import { EncryptedKey } from "../Types"; export interface BrowserStorageConfigParams { prefix?: string; storage: { - get: (key?: string | string[] | object) => Promise; + get: ( + key?: null | string | string[] | Record, + ) => Promise>; remove: (key: string | string[]) => Promise; - set: (items: object) => Promise; + set: (items: Record) => Promise; }; } diff --git a/@stellar/typescript-wallet-sdk-km/src/Types/index.ts b/@stellar/typescript-wallet-sdk-km/src/Types/index.ts index c9840cd..21e17bc 100644 --- a/@stellar/typescript-wallet-sdk-km/src/Types/index.ts +++ b/@stellar/typescript-wallet-sdk-km/src/Types/index.ts @@ -179,6 +179,7 @@ export interface GetAuthTokenParams { authServer: string; authServerHomeDomains: [string]; authServerKey: string; + challengeToken?: string; account?: string; clientDomain?: string; onChallengeTransactionSignature?: (tx: Transaction) => Promise; diff --git a/@stellar/typescript-wallet-sdk-km/src/keyManager.ts b/@stellar/typescript-wallet-sdk-km/src/keyManager.ts index 042e93c..dbc02fd 100644 --- a/@stellar/typescript-wallet-sdk-km/src/keyManager.ts +++ b/@stellar/typescript-wallet-sdk-km/src/keyManager.ts @@ -251,6 +251,7 @@ export class KeyManager { password, authServer, authServerKey, + challengeToken, authServerHomeDomains, clientDomain, onChallengeTransactionSignature = (tx: Transaction) => @@ -302,7 +303,12 @@ export class KeyManager { challengeUrl += `&client_domain=${encodeURIComponent(clientDomain)}`; } - const challengeRes = await fetch(challengeUrl); + let headers = {}; + if (challengeToken) { + headers = { Authorization: `Bearer ${challengeToken}` }; + } + + const challengeRes = await fetch(challengeUrl, { headers }); if (challengeRes.status !== 200) { const challengeText = await challengeRes.text(); diff --git a/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts b/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts index a24b83e..c9b4060 100644 --- a/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts +++ b/@stellar/typescript-wallet-sdk-km/test/keyManager.test.ts @@ -9,6 +9,10 @@ import { TimeoutInfinite, BASE_FEE, } from "@stellar/stellar-sdk"; +import { + DefaultAuthHeaderSigner, + SigningKeypair, +} from "@stellar/typescript-wallet-sdk"; import { mockRandomForEach } from "jest-mock-random"; import randomBytes from "randombytes"; import sinon from "sinon"; @@ -500,6 +504,7 @@ describe("fetchAuthToken", () => { "https://www.stellar.org/auth?account=" + "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + "&client_domain=example.com", + { headers: {} }, ); }); @@ -615,6 +620,109 @@ describe("fetchAuthToken", () => { expect(res).toBe(token); }); + test("Can use a challenge token", async () => { + const authServer = "https://www.stellar.org/auth"; + const password = "very secure password"; + + const keyNetwork = Networks.TESTNET; + + const token = "👍"; + const accountKey = Keypair.random(); + const account = new Account(accountKey.publicKey(), "-1"); + + // set up the manager + const testStore = new MemoryKeyStore(); + const testKeyManager = new KeyManager({ + keyStore: testStore, + }); + + testKeyManager.registerEncrypter(IdentityEncrypter); + + const keypair = Keypair.master(keyNetwork); + + // A Base64 digit represents 6 bits, to generate a random 64 bytes + // base64 string, we need 48 random bytes = (64 * 6)/8 + // + // Each Base64 digit is in ASCII and each ASCII characters when + // turned into binary represents 8 bits = 1 bytes. + const value = randomBytes(48).toString("base64"); + + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: keyNetwork, + }) + .addOperation( + Operation.manageData({ + name: `stellar.org auth`, + value, + source: keypair.publicKey(), + }), + ) + .addOperation( + Operation.manageData({ + name: "web_auth_domain", + value: new URL(authServer).hostname, + source: account.accountId(), + }), + ) + .setTimeout(300) + .build(); + + tx.sign(accountKey); + + jest + .spyOn(global, "fetch") + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + transaction: tx.toXDR(), + network_passphrase: keyNetwork, + }), + ), + ) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + token, + status: 1, + message: "Good job friend", + }), + ), + ); + + // save this key + const keyMetadata = await testKeyManager.storeKey({ + key: { + type: KeyType.plaintextKey, + publicKey: keypair.publicKey(), + privateKey: keypair.secret(), + network: keyNetwork, + }, + password, + encrypterName: "IdentityEncrypter", + }); + + // create a challengeToken + const authHeaderSigner = new DefaultAuthHeaderSigner(); + const challengeToken = await authHeaderSigner.createToken({ + claims: {}, + issuer: new SigningKeypair(accountKey), + }); + + await testKeyManager.fetchAuthToken({ + id: keyMetadata.id, + password, + authServer, + challengeToken, + authServerKey: account.accountId(), + authServerHomeDomains: ["stellar.org"], + }); + + expect( + (global.fetch as any).mock.calls[0][1].headers["Authorization"], + ).toBeTruthy(); + }); + test("Rejects TXs with non-zero seq numbers", async () => { const authServer = "https://www.stellar.org/auth"; const password = "very secure password"; diff --git a/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD b/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD new file mode 100644 index 0000000..bb5f6e9 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD @@ -0,0 +1,8 @@ +# Release notes - Typescript Wallet SDK Key Soroban - 1.5.0 + +### Added +* Init to the project, added soroban functionality +* getTokenInvocationArgs function +* Token parsing/formatting functions +* scValyByType function +* generic getInvocationDetails helper diff --git a/@stellar/typescript-wallet-sdk-soroban/README.md b/@stellar/typescript-wallet-sdk-soroban/README.md new file mode 100644 index 0000000..b2e6b19 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/README.md @@ -0,0 +1,80 @@ +# Stellar Typescript Wallet Soroban SDK [![npm version](https://badge.fury.io/js/@stellar%2Ftypescript-wallet-sdk-soroban.svg)](https://badge.fury.io/js/@stellar%2Ftypescript-wallet-sdk-soroban) + +The Typescript Wallet Soroban SDK is a work-in-progress library that (currently) +allows developers to use soroban helpers in their wallet applications. It works +in conjuction with the main +[Typescript Wallet SDK](https://github.com/stellar/typescript-wallet-sdk) to +hold all the functionality a developer would need to create a wallet for the +stellar network. + +## Dependency + +The library is available via npm. To import `typescript-wallet-sdk-soroban` you +need to add it as a dependency to your code: + +yarn: + +```shell +yarn add @stellar/typescript-wallet-sdk-soroban +``` + +npm: + +```shell +npm install @stellar/typescript-wallet-sdk-soroban +``` + +## Introduction + +Here's some examples on how to use the Soroban helpers: + +```typescript +import { + getTokenInvocationArgs, + formatTokenAmount, + parseTokenAmount, + scValByType, +} from "@stellar/typescript-wallet-sdk-soroban"; + +const transaction = TransactionBuilder.fromXDR( + "AAAAAgAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrDucaFsAAAWIAAAAMQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABHkEVdJ+UfDnWpBr/qF582IEoDQ0iW0WPzO9CEUdvvh8AAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAAjOiEfRh4kaFVQDu/CSTZLMtnyg0DbNowZ/G2nLES3KwAAAASAAAAAAAAAADoFl2ACT9HZkbCeuaT9MAIdStpdf58wM3P24nl738AnQAAAAoAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAACHRyYW5zZmVyAAAAAwAAABIAAAAAAAAAAIzohH0YeJGhVUA7vwkk2SzLZ8oNA2zaMGfxtpyxEtysAAAAEgAAAAAAAAAA6BZdgAk/R2ZGwnrmk/TACHUraXX+fMDNz9uJ5e9/AJ0AAAAKAAAAAAAAAAAAAAAAAAAABQAAAAAAAAABAAAAAAAAAAIAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAFAAAAAEAAAAHa35L+/RxV6EuJOVk78H5rCN+eubXBWtsKrRxeLnnpRAAAAACAAAABgAAAAEeQRV0n5R8OdakGv+oXnzYgSgNDSJbRY/M70IRR2++HwAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrAAAAAEAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAEAAAAAEAAAACAAAADwAAAAdCYWxhbmNlAAAAABIAAAAAAAAAAOgWXYAJP0dmRsJ65pP0wAh1K2l1/nzAzc/bieXvfwCdAAAAAQBkcwsAACBwAAABKAAAAAAAAB1kAAAAAA==", + Networks.FUTURENET, +) as Transaction, Operation.InvokeHostFunction[]>; +const op = transaction.operations[0]; + +const args = getTokenInvocationArgs(op); +/* + extracts args from the invoke host function operation: + args = { + fnName: "transfer, + contractId: "CAPECFLUT6KHYOOWUQNP7KC6PTMICKANBURFWRMPZTXUEEKHN67B7UI2", + from: "GCGORBD5DB4JDIKVIA536CJE3EWMWZ6KBUBWZWRQM7Y3NHFRCLOKYVAL", + to: "GDUBMXMABE7UOZSGYJ5ONE7UYAEHKK3JOX7HZQGNZ7NYTZPPP4AJ2GQJ", + amount: 5 + } +*/ + +const formattedAmount = formatTokenAmount("10000123", 3); +// converts smart contract token amount into a displayable amount that can be +// used on client UI +// formattedAmount = "10000.123" + +const parsedAmount = parseTokenAmount("10000.123", 3); +// converts an amount to a whole (bigint) number that can be used on +// smart contracts operations +// parsedAmount = 10000123 + +const accountAddress = xdr.ScVal.scvAddress( + xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519( + StrKey.decodeEd25519PublicKey( + "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB", + ), + ), + ), +); + +const addressString = scValByType(accountAddress); +// converts smart contract complex value into a simple string +// addressString = "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB" +``` diff --git a/@stellar/typescript-wallet-sdk-soroban/babel.config.js b/@stellar/typescript-wallet-sdk-soroban/babel.config.js new file mode 100644 index 0000000..647701a --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/babel.config.js @@ -0,0 +1,5 @@ +const parentConfig = require("../../babel.config"); + +module.exports = { + ...parentConfig, +}; diff --git a/@stellar/typescript-wallet-sdk-soroban/package.json b/@stellar/typescript-wallet-sdk-soroban/package.json new file mode 100644 index 0000000..30f1cfe --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/package.json @@ -0,0 +1,41 @@ +{ + "name": "@stellar/typescript-wallet-sdk-soroban", + "version": "1.5.0", + "engines": { + "node": ">=18" + }, + "browser": "./lib/bundle_browser.js", + "main": "./lib/bundle.js", + "types": "./lib/index.d.ts", + "license": "Apache-2.0", + "private": false, + "devDependencies": { + "@babel/preset-env": "^7.24.0", + "@babel/preset-typescript": "^7.23.3", + "@stellar/prettier-config": "^1.0.1", + "@stellar/tsconfig": "^1.0.2", + "@types/jest": "^29.5.12", + "@typescript-eslint/parser": "^7.1.1", + "babel-jest": "^29.7.0", + "husky": "^9.0.11", + "jest": "^29.7.0", + "npm-run-all": "^4.1.5", + "ts-jest": "^29.1.2", + "ts-loader": "^9.5.1", + "tslib": "^2.6.2", + "typescript": "^5.3.3", + "webpack": "^5.90.3", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "@stellar/stellar-sdk": "^11.1.0" + }, + "scripts": { + "prepare": "husky install", + "test": "jest --watchAll", + "test:ci": "jest --ci", + "build:web": "webpack --config webpack.config.js", + "build:node": "webpack --env NODE=true --config webpack.config.js", + "build": "run-p build:web build:node" + } +} diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/formatTokenAmount.ts b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/formatTokenAmount.ts new file mode 100644 index 0000000..0fd8f0c --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/formatTokenAmount.ts @@ -0,0 +1,23 @@ +import { Soroban } from "@stellar/stellar-sdk"; + +/** + * https://github.com/stellar/js-stellar-base/blob/4b510113738aefb5decb31e2ae72c27da5dd7f5c/src/soroban.js + * + * Given a whole number smart contract amount of a token and an amount of + * decimal places (if the token has any), it returns a "display" value. + * + * All arithmetic inside the contract is performed on integers to avoid + * potential precision and consistency issues of floating-point. + * + * @param {string | bigint} amount the token amount you want to display + * @param {number} decimals specify how many decimal places a token has + * + * @returns {string} the display value + * @throws {TypeError} if the given amount has a decimal point already + * @example + * formatTokenAmount("123000", 4) === "12.3"; + */ +export const formatTokenAmount = ( + amount: string | bigint, + decimals: number, +): string => Soroban.formatTokenAmount(amount.toString(), decimals); diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/getInvocationDetails.ts b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/getInvocationDetails.ts new file mode 100644 index 0000000..422e68e --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/getInvocationDetails.ts @@ -0,0 +1,81 @@ +import { Address, Asset, StrKey, xdr } from "@stellar/stellar-sdk"; + +import { InvocationArgs } from "Types"; + +/** + * Extract invocation args and params from a Soroban authorized invocation + * tree up to its immediate sub invocations. + * + * @param {xdr.SorobanAuthorizedInvocation} invocationTree - The invocation tree. + * + * @returns {InvocationArgs[]} A list of user friendly invocation args and params. + */ +export const getInvocationDetails = ( + invocationTree: xdr.SorobanAuthorizedInvocation, +): InvocationArgs[] => { + const invocations = [ + getInvocationArgs(invocationTree), + ...invocationTree.subInvocations().map(getInvocationArgs), + ]; + return invocations.filter(isInvocationArg); +}; + +const isInvocationArg = ( + invocation: InvocationArgs | undefined, +): invocation is InvocationArgs => !!invocation; + +export const getInvocationArgs = ( + invocation: xdr.SorobanAuthorizedInvocation, +): InvocationArgs | undefined => { + const fn = invocation.function(); + + switch (fn.switch().value) { + // sorobanAuthorizedFunctionTypeContractFn + case 0: { + const _invocation = fn.contractFn(); + const contractId = StrKey.encodeContract( + _invocation.contractAddress().contractId(), + ); + const fnName = _invocation.functionName().toString(); + const args = _invocation.args(); + return { fnName, contractId, args, type: "invoke" }; + } + + // sorobanAuthorizedFunctionTypeCreateContractHostFn + case 1: { + const _invocation = fn.createContractHostFn(); + const [exec, preimage] = [ + _invocation.executable(), + _invocation.contractIdPreimage(), + ]; + + switch (exec.switch().value) { + // contractExecutableWasm + case 0: { + const details = preimage.fromAddress(); + + return { + type: "wasm", + salt: details.salt().toString("hex"), + hash: exec.wasmHash().toString("hex"), + address: Address.fromScAddress(details.address()).toString(), + }; + } + + // contractExecutableStellarAsset + case 1: + return { + type: "sac", + asset: Asset.fromOperation(preimage.fromAsset()).toString(), + }; + + default: + throw new Error(`unknown creation type: ${JSON.stringify(exec)}`); + } + } + + default: { + return undefined; + } + } +}; diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/getTokenInvocationArgs.ts b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/getTokenInvocationArgs.ts new file mode 100644 index 0000000..356109c --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/getTokenInvocationArgs.ts @@ -0,0 +1,94 @@ +import { Operation, StrKey, scValToNative, xdr } from "@stellar/stellar-sdk"; + +import { + ArgsForTokenInvocation, + SorobanTokenInterface, + TokenInvocationArgs, +} from "../Types"; + +export const getArgsForTokenInvocation = ( + fnName: string, + args: xdr.ScVal[], +): ArgsForTokenInvocation => { + let amount: bigint | number; + let from = ""; + let to = ""; + + switch (fnName) { + case SorobanTokenInterface.transfer: + from = StrKey.encodeEd25519PublicKey( + args[0].address().accountId().ed25519(), + ); + to = StrKey.encodeEd25519PublicKey( + args[1].address().accountId().ed25519(), + ); + amount = scValToNative(args[2]); + break; + case SorobanTokenInterface.mint: + to = StrKey.encodeEd25519PublicKey( + args[0].address().accountId().ed25519(), + ); + amount = scValToNative(args[1]); + break; + default: + amount = BigInt(0); + } + + return { from, to, amount }; +}; + +/** + * Get params and args related to the invoked contract. It must use a valid + * "transfer" or "mint" invocation otherwise it will return 'null'. + * + * @param {Operation.InvokeHostFunction} hostFn - The invoke host function. + * + * @returns {TokenInvocationArgs | null} Params and args related to the + * "transfer" or "mint" invocation like function name, contract id, from/to + * addresses and amount. + */ +export const getTokenInvocationArgs = ( + hostFn: Operation.InvokeHostFunction, +): TokenInvocationArgs | null => { + if (!hostFn?.func?.invokeContract) { + return null; + } + + let invokedContract: xdr.InvokeContractArgs; + + try { + invokedContract = hostFn.func.invokeContract(); + } catch (e) { + return null; + } + + const contractId = StrKey.encodeContract( + invokedContract.contractAddress().contractId(), + ); + const fnName = invokedContract + .functionName() + .toString() as SorobanTokenInterface; + const args = invokedContract.args(); + + if ( + ![SorobanTokenInterface.transfer, SorobanTokenInterface.mint].includes( + fnName, + ) + ) { + return null; + } + + let opArgs: ArgsForTokenInvocation; + + try { + opArgs = getArgsForTokenInvocation(fnName, args); + } catch (e) { + return null; + } + + return { + fnName, + contractId, + ...opArgs, + }; +}; diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/index.ts b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/index.ts new file mode 100644 index 0000000..8686f5e --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/index.ts @@ -0,0 +1,5 @@ +export * from "./formatTokenAmount"; +export * from "./getInvocationDetails"; +export * from "./getTokenInvocationArgs"; +export * from "./parseTokenAmount"; +export * from "./scValByType"; diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/parseTokenAmount.ts b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/parseTokenAmount.ts new file mode 100644 index 0000000..8f131d4 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/parseTokenAmount.ts @@ -0,0 +1,33 @@ +import { Soroban } from "@stellar/stellar-sdk"; +import BigNumber from "bignumber.js"; + +/** + * https://github.com/stellar/js-stellar-base/blob/4b510113738aefb5decb31e2ae72c27da5dd7f5c/src/soroban.js + * + * Parse a token amount to use it on smart contract + * + * This function takes the display value and its decimals (if the token has + * any) and returns a string that'll be used within the smart contract. + * + * @param {string | number | BigNumber} amount the token amount you want to + * use in a smart contract which you've been displaying in a UI + * @param {number} decimals the number of decimal places expected in the + * display value (different than the "actual" number, because suffix zeroes + * might not be present) + * + * @returns {bigint} the whole number token amount represented by the display + * value with the decimal places shifted over + * + * @example + * const displayValueAmount = "123.4560" + * const parsedAmtForSmartContract = parseTokenAmount(displayValueAmount, 5); + * parsedAmtForSmartContract === "12345600" + */ +export const parseTokenAmount = ( + amount: string | number | BigNumber, + decimals: number, +): bigint => { + const parsedAmount = Soroban.parseTokenAmount(amount.toString(), decimals); + + return BigInt(parsedAmount.toString()); +}; diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts new file mode 100644 index 0000000..742a8cc --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/Helpers/scValByType.ts @@ -0,0 +1,97 @@ +import { StrKey, scValToNative, xdr } from "@stellar/stellar-sdk"; + +/* eslint-disable jsdoc/require-returns-type */ +/** + * This function attempts to convert smart contract (complex) value types + * to common/simpler types like string, array, buffer, JSON string, etc. + * + * @param {xdr.ScVal} scVal the smart contract (complex) value + * + * + * @returns the smart contract value converted to a common/simpler + * value like string, array, buffer, JSON string, etc. + * + * @example + * const accountAddress = xdr.ScVal.scvAddress( + * xdr.ScAddress.scAddressTypeAccount( + * xdr.PublicKey.publicKeyTypeEd25519( + * StrKey.decodeEd25519PublicKey("GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB"), + * ), + * ) + * ); ===> complex object + * + * scValByType(accountAddress) returns "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB" + */ +export const scValByType = (scVal: xdr.ScVal) => { + switch (scVal.switch()) { + case xdr.ScValType.scvAddress(): { + const address = scVal.address(); + const addressType = address.switch(); + if (addressType.name === "scAddressTypeAccount") { + return StrKey.encodeEd25519PublicKey(address.accountId().ed25519()); + } + return StrKey.encodeContract(address.contractId()); + } + + case xdr.ScValType.scvBool(): { + return scVal.b(); + } + + case xdr.ScValType.scvBytes(): { + return JSON.stringify(scVal.bytes().toJSON().data); + } + + case xdr.ScValType.scvContractInstance(): { + const instance = scVal.instance(); + return instance.executable().wasmHash()?.toString(); + } + + case xdr.ScValType.scvError(): { + const error = scVal.error(); + return error.value(); + } + + case xdr.ScValType.scvTimepoint(): + case xdr.ScValType.scvDuration(): + case xdr.ScValType.scvI128(): + case xdr.ScValType.scvI256(): + case xdr.ScValType.scvI32(): + case xdr.ScValType.scvI64(): + case xdr.ScValType.scvU128(): + case xdr.ScValType.scvU256(): + case xdr.ScValType.scvU32(): + case xdr.ScValType.scvU64(): { + return scValToNative(scVal).toString(); + } + + case xdr.ScValType.scvLedgerKeyNonce(): + case xdr.ScValType.scvLedgerKeyContractInstance(): { + if (scVal.switch().name === "scvLedgerKeyNonce") { + const val = scVal.nonceKey().nonce(); + return val.toString(); + } + return scVal.value(); + } + + case xdr.ScValType.scvVec(): + case xdr.ScValType.scvMap(): { + return JSON.stringify( + scValToNative(scVal), + (_, val) => (typeof val === "bigint" ? val.toString() : val), + 2, + ); + } + + case xdr.ScValType.scvString(): + case xdr.ScValType.scvSymbol(): { + const native = scValToNative(scVal); + if (native.constructor === "Uint8Array") { + return native.toString(); + } + return native; + } + + default: + return null; + } +}; diff --git a/@stellar/typescript-wallet-sdk-soroban/src/Types/index.ts b/@stellar/typescript-wallet-sdk-soroban/src/Types/index.ts new file mode 100644 index 0000000..12dc1be --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/Types/index.ts @@ -0,0 +1,39 @@ +import { xdr } from "@stellar/stellar-sdk"; + +// https://github.com/stellar/soroban-examples/blob/main/token/src/contract.rs +export enum SorobanTokenInterface { + transfer = "transfer", + mint = "mint", +} + +export type ArgsForTokenInvocation = { + from: string; + to: string; + amount: bigint | number; +}; + +export type TokenInvocationArgs = ArgsForTokenInvocation & { + fnName: SorobanTokenInterface; + contractId: string; +}; + +export interface FnArgsInvoke { + type: "invoke"; + fnName: string; + contractId: string; + args: xdr.ScVal[]; +} + +export interface FnArgsCreateWasm { + type: "wasm"; + salt: string; + hash: string; + address: string; +} + +export interface FnArgsCreateSac { + type: "sac"; + asset: string; +} + +export type InvocationArgs = FnArgsInvoke | FnArgsCreateWasm | FnArgsCreateSac; diff --git a/@stellar/typescript-wallet-sdk-soroban/src/index.ts b/@stellar/typescript-wallet-sdk-soroban/src/index.ts new file mode 100644 index 0000000..f248091 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/src/index.ts @@ -0,0 +1,2 @@ +export * from "./Helpers"; +export * from "./Types"; diff --git a/@stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts b/@stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts new file mode 100644 index 0000000..e3b385a --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/test/helpers.test.ts @@ -0,0 +1,555 @@ +import { + Address, + Asset, + Memo, + MemoType, + Networks, + Operation, + StrKey, + Transaction, + TransactionBuilder, + xdr, +} from "@stellar/stellar-sdk"; +import BigNumber from "bignumber.js"; + +import { + SorobanTokenInterface, + formatTokenAmount, + getInvocationDetails, + getTokenInvocationArgs, + parseTokenAmount, + scValByType, +} from "../src"; +import { makeInvocation, randomContracts, randomKey } from "./utils"; + +const transactions = { + classic: + "AAAAAgAAAACCMXQVfkjpO2gAJQzKsUsPfdBCyfrvy7sr8+35cOxOSwAAAGQABqQMAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAACCMXQVfkjpO2gAJQzKsUsPfdBCyfrvy7sr8+35cOxOSwAAAAAAmJaAAAAAAAAAAAFw7E5LAAAAQBu4V+/lttEONNM6KFwdSf5TEEogyEBy0jTOHJKuUzKScpLHyvDJGY+xH9Ri4cIuA7AaB8aL+VdlucCfsNYpKAY=", + sorobanTransfer: + "AAAAAgAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrDucaFsAAAWIAAAAMQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABHkEVdJ+UfDnWpBr/qF582IEoDQ0iW0WPzO9CEUdvvh8AAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAAjOiEfRh4kaFVQDu/CSTZLMtnyg0DbNowZ/G2nLES3KwAAAASAAAAAAAAAADoFl2ACT9HZkbCeuaT9MAIdStpdf58wM3P24nl738AnQAAAAoAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAACHRyYW5zZmVyAAAAAwAAABIAAAAAAAAAAIzohH0YeJGhVUA7vwkk2SzLZ8oNA2zaMGfxtpyxEtysAAAAEgAAAAAAAAAA6BZdgAk/R2ZGwnrmk/TACHUraXX+fMDNz9uJ5e9/AJ0AAAAKAAAAAAAAAAAAAAAAAAAABQAAAAAAAAABAAAAAAAAAAIAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAFAAAAAEAAAAHa35L+/RxV6EuJOVk78H5rCN+eubXBWtsKrRxeLnnpRAAAAACAAAABgAAAAEeQRV0n5R8OdakGv+oXnzYgSgNDSJbRY/M70IRR2++HwAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrAAAAAEAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAEAAAAAEAAAACAAAADwAAAAdCYWxhbmNlAAAAABIAAAAAAAAAAOgWXYAJP0dmRsJ65pP0wAh1K2l1/nzAzc/bieXvfwCdAAAAAQBkcwsAACBwAAABKAAAAAAAAB1kAAAAAA==", + sorobanMint: + "AAAAAgAAAACM6IR9GHiRoVVAO78JJNksy2fKDQNs2jBn8bacsRLcrDucQIQAAAWIAAAAMQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABHkEVdJ+UfDnWpBr/qF582IEoDQ0iW0WPzO9CEUdvvh8AAAAEbWludAAAAAIAAAASAAAAAAAAAADoFl2ACT9HZkbCeuaT9MAIdStpdf58wM3P24nl738AnQAAAAoAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAAAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAABG1pbnQAAAACAAAAEgAAAAAAAAAA6BZdgAk/R2ZGwnrmk/TACHUraXX+fMDNz9uJ5e9/AJ0AAAAKAAAAAAAAAAAAAAAAAAAABQAAAAAAAAABAAAAAAAAAAIAAAAGAAAAAR5BFXSflHw51qQa/6hefNiBKA0NIltFj8zvQhFHb74fAAAAFAAAAAEAAAAHa35L+/RxV6EuJOVk78H5rCN+eubXBWtsKrRxeLnnpRAAAAABAAAABgAAAAEeQRV0n5R8OdakGv+oXnzYgSgNDSJbRY/M70IRR2++HwAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAADoFl2ACT9HZkbCeuaT9MAIdStpdf58wM3P24nl738AnQAAAAEAYpBIAAAfrAAAAJQAAAAAAAAdYwAAAAA=", +}; + +describe("getTokenInvocationArgs for different function names", () => { + it("get token invocation args for Soroban transfer (payment) operation", () => { + const transaction = TransactionBuilder.fromXDR( + transactions.sorobanTransfer, + Networks.FUTURENET, + ) as Transaction, Operation.InvokeHostFunction[]>; + const op = transaction.operations[0]; + + const args = getTokenInvocationArgs(op); + + expect(args.fnName).toBe(SorobanTokenInterface.transfer); + expect(args.contractId).toBe( + "CAPECFLUT6KHYOOWUQNP7KC6PTMICKANBURFWRMPZTXUEEKHN67B7UI2", + ); + expect(args.from).toBe( + "GCGORBD5DB4JDIKVIA536CJE3EWMWZ6KBUBWZWRQM7Y3NHFRCLOKYVAL", + ); + expect(args.to).toBe( + "GDUBMXMABE7UOZSGYJ5ONE7UYAEHKK3JOX7HZQGNZ7NYTZPPP4AJ2GQJ", + ); + expect(args.amount === BigInt(5)).toBeTruthy(); + }); + + it("get token invocation args for Soroban mint operation", () => { + const transaction = TransactionBuilder.fromXDR( + transactions.sorobanMint, + Networks.FUTURENET, + ) as Transaction, Operation.InvokeHostFunction[]>; + const op = transaction.operations[0]; + + const args = getTokenInvocationArgs(op); + + expect(args.fnName).toBe(SorobanTokenInterface.mint); + expect(args.contractId).toBe( + "CAPECFLUT6KHYOOWUQNP7KC6PTMICKANBURFWRMPZTXUEEKHN67B7UI2", + ); + expect(args.from).toBe(""); + expect(args.to).toBe( + "GDUBMXMABE7UOZSGYJ5ONE7UYAEHKK3JOX7HZQGNZ7NYTZPPP4AJ2GQJ", + ); + expect(args.amount === BigInt(5)).toBeTruthy(); + }); + + it("stellar classic transaction should have no token invocation args", () => { + const transaction = TransactionBuilder.fromXDR( + transactions.classic, + Networks.TESTNET, + ) as Transaction, Operation.InvokeHostFunction[]>; + const op = transaction.operations[0]; + + const args = getTokenInvocationArgs(op); + + expect(args).toBe(null); + }); +}); + +describe("Token formatting and parsing functions", () => { + it("should format different types of token amount values", () => { + const formatted = "1000000.1234567"; + + const value1 = BigInt(10000001234567); + expect(formatTokenAmount(value1, 7)).toEqual(formatted); + + const value2 = "10000001234567"; + expect(formatTokenAmount(value2, 7)).toEqual(formatted); + }); + + it("should parse different types of token amount values", () => { + const parsed = BigInt(10000001234567); + + const value1 = "1000000.1234567"; + expect(parseTokenAmount(value1, 7) === parsed).toBeTruthy(); + + const value2 = 1000000.1234567; + expect(parseTokenAmount(value2, 7) === parsed).toBeTruthy(); + + const value3 = new BigNumber("1000000.1234567"); + expect(parseTokenAmount(value3, 7) === parsed).toBeTruthy(); + }); +}); + +describe("scValByType should render expected common types", () => { + it("should render addresses as strings", () => { + const ACCOUNT = "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB"; + const CONTRACT = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"; + + const scAddressAccount = xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519( + StrKey.decodeEd25519PublicKey(ACCOUNT), + ), + ); + const accountAddress = xdr.ScVal.scvAddress(scAddressAccount); + const parsedAccountAddress = scValByType(accountAddress); + expect(parsedAccountAddress).toEqual(ACCOUNT); + + const scAddressContract = xdr.ScAddress.scAddressTypeContract( + StrKey.decodeContract(CONTRACT), + ); + const contractAddress = xdr.ScVal.scvAddress(scAddressContract); + const parsedContractAddress = scValByType(contractAddress); + expect(parsedContractAddress).toEqual(CONTRACT); + }); + + it("should render booleans as booleans", () => { + const bool = xdr.ScVal.scvBool(true); + const parsedBool = scValByType(bool); + expect(parsedBool).toEqual(true); + }); + + it("should render bytes as a stringified array of numbers", () => { + const bytesBuffer = Buffer.from([0x00, 0x01]); + const bytes = xdr.ScVal.scvBytes(bytesBuffer); + const parsedBytes = scValByType(bytes); + expect(parsedBytes).toEqual("[0,1]"); + }); + + it("should render contract instance as string", () => { + // Note: those are totally random values for 'executable' and 'storage' + const contractInstance = xdr.ScVal.scvContractInstance( + new xdr.ScContractInstance({ + executable: xdr.ContractExecutable.contractExecutableWasm( + Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]), + ), + storage: [ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvString("keyOne"), + val: xdr.ScVal.scvU64(new xdr.Uint64(123)), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvString("keyTwo"), + val: xdr.ScVal.scvU64(new xdr.Uint64(456)), + }), + ], + }), + ); + + const parsedContractInstance = scValByType(contractInstance); + expect(parsedContractInstance).toEqual( + contractInstance.instance().executable().wasmHash()?.toString(), + ); + }); + + it("should render an error as a number or a ScErrorCode object including the contract name and code", () => { + const contractErrorCode = 1; + const contractError = xdr.ScError.sceContract(contractErrorCode); + const scvContractError = xdr.ScVal.scvError(contractError); + const parsedContractError = scValByType(scvContractError); + expect(parsedContractError).toEqual(contractErrorCode); + + const scErrorCode = xdr.ScErrorCode.scecExceededLimit(); + const wasmError = xdr.ScError.sceWasmVm(scErrorCode); + const scvWasmError = xdr.ScVal.scvError(wasmError); + const parsedWasmError = scValByType(scvWasmError); + expect(parsedWasmError).toEqual(scErrorCode); + expect(parsedWasmError.name).toEqual("scecExceededLimit"); + expect(parsedWasmError.value).toEqual(5); + }); + + it("should render all numeric types as strings", () => { + const scv1 = xdr.ScVal.scvTimepoint(new xdr.Uint64(123)); + const parsedScv1 = scValByType(scv1); + expect(parsedScv1).toEqual("123"); + + const scv2 = xdr.ScVal.scvDuration(new xdr.Uint64(456)); + const parsedScv2 = scValByType(scv2); + expect(parsedScv2).toEqual("456"); + + const scv3 = xdr.ScVal.scvI128( + new xdr.Int128Parts({ + hi: new xdr.Int64(789), + lo: new xdr.Uint64(123), + }), + ); + const parsedScv3 = scValByType(scv3); + // This is a complex numeric type which would result something + // like "14554481074156836225147" so let's simply check it's type + expect(typeof parsedScv3 === "string").toBeTruthy(); + + const scv4 = xdr.ScVal.scvI256( + new xdr.Int256Parts({ + hiHi: new xdr.Int64(7899), + hiLo: new xdr.Uint64(7890), + loHi: new xdr.Uint64(1239), + loLo: new xdr.Uint64(1230), + }), + ); + const parsedScv4 = scValByType(scv4); + // This is a complex numeric type which would result something + // like "49582826607819391356223728528923561497541386824365385940206798" + // so let's simply check it's type + expect(typeof parsedScv4 === "string").toBeTruthy(); + + const scv5 = xdr.ScVal.scvI32(3232); + const parsedScv5 = scValByType(scv5); + expect(parsedScv5).toEqual("3232"); + + const scv6 = xdr.ScVal.scvI64(new xdr.Int64(6464)); + const parsedScv6 = scValByType(scv6); + expect(parsedScv6).toEqual("6464"); + + const scv7 = xdr.ScVal.scvU128( + new xdr.UInt128Parts({ + hi: new xdr.Uint64(1288), + lo: new xdr.Uint64(1280), + }), + ); + const parsedScv7 = scValByType(scv7); + // This is a complex numeric type which would result something + // like "23759406366937902482688" so let's simply check it's type + expect(typeof parsedScv7 === "string").toBeTruthy(); + + const scv8 = xdr.ScVal.scvU256( + new xdr.Int256Parts({ + hiHi: new xdr.Uint64(25699), + hiLo: new xdr.Uint64(25600), + loHi: new xdr.Uint64(2569), + loLo: new xdr.Uint64(2560), + }), + ); + const parsedScv8 = scValByType(scv8); + // This is a complex numeric type which would result something + // like "161315237497702308958527180980189843892124212203059848998291968" + // so let's simply check it's type + expect(typeof parsedScv8 === "string").toBeTruthy(); + + const scv9 = xdr.ScVal.scvU32(323232); + const parsedScv9 = scValByType(scv9); + expect(parsedScv9).toEqual("323232"); + + const scv10 = xdr.ScVal.scvU64(new xdr.Uint64(646464)); + const parsedScv10 = scValByType(scv10); + expect(parsedScv10).toEqual("646464"); + }); + + it("should render nonce ledger key as string", () => { + const nonce = new xdr.Int64(123); + const nonceKey = new xdr.ScNonceKey({ nonce }); + const ledgerKey = xdr.ScVal.scvLedgerKeyNonce(nonceKey); + const parsedLedgerKey = scValByType(ledgerKey); + expect(parsedLedgerKey).toEqual("123"); + + const ledgerKeyContractInstance = xdr.ScVal.scvLedgerKeyContractInstance(); + const parsedInstance = scValByType(ledgerKeyContractInstance); + expect(parsedInstance).toEqual(undefined); + }); + + it("should render vectors and maps as JSON strings", () => { + const xdrVec = xdr.ScVal.scvVec([ + xdr.ScVal.scvU64(new xdr.Uint64(123)), + xdr.ScVal.scvU64(new xdr.Uint64(321)), + ]); + const parsedVec = scValByType(xdrVec); + expect(parsedVec).toBe( + JSON.stringify( + ["123", "321"], + (_, val) => (typeof val === "bigint" ? val.toString() : val), + 2, + ), + ); + + const xdrMap = xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvString("keyOne"), + val: xdr.ScVal.scvU64(new xdr.Uint64(456)), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvString("keyTwo"), + val: xdr.ScVal.scvU64(new xdr.Uint64(789)), + }), + ]); + const parsedMap = scValByType(xdrMap); + expect(parsedMap).toBe( + JSON.stringify( + { keyOne: "456", keyTwo: "789" }, + (_, val) => (typeof val === "bigint" ? val.toString() : val), + 2, + ), + ); + }); + + it("should possibly render strings and symbols as strings", () => { + const scvString = xdr.ScVal.scvString("any string"); + const parsedString = scValByType(scvString); + expect(parsedString).toEqual("any string"); + + const scvSym = xdr.ScVal.scvSymbol("some crazy symbol"); + const parsedSymbol = scValByType(scvSym); + expect(parsedSymbol).toEqual("some crazy symbol"); + }); + + it("should render void as null", () => { + const scvVoid = xdr.ScVal.scvVoid(); + const parsedVoid = scValByType(scvVoid); + expect(parsedVoid).toEqual(null); + }); +}); + +describe("getInvocationDetails for a Soroban Authorized Invocation tree", () => { + const [nftContract, swapContract, xlmContract, usdcContract] = + randomContracts(4); + + const nftId = randomKey(); + const usdcId = randomKey(); + const invoker = randomKey(); + const dest = randomKey(); + + const rootSubInvocations = [ + new xdr.SorobanAuthorizedInvocation({ + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeCreateContractHostFn( + new xdr.CreateContractArgs({ + contractIdPreimage: + xdr.ContractIdPreimage.contractIdPreimageFromAsset( + new Asset("TEST", nftId).toXDRObject(), + ), + executable: xdr.ContractExecutable.contractExecutableStellarAsset(), + }), + ), + subInvocations: [], + }), + new xdr.SorobanAuthorizedInvocation({ + function: makeInvocation( + swapContract, + "swap", + "native", + `USDC:${usdcId}`, + new Address(invoker).toScVal(), + new Address(dest).toScVal(), + ), + subInvocations: [ + new xdr.SorobanAuthorizedInvocation({ + function: makeInvocation( + xlmContract, + "transfer", + new Address(invoker).toScVal(), + "7", + ), + subInvocations: [], + }), + new xdr.SorobanAuthorizedInvocation({ + function: makeInvocation( + usdcContract, + "transfer", + new Address(invoker).toScVal(), + "1", + ), + subInvocations: [], + }), + ], + }), + new xdr.SorobanAuthorizedInvocation({ + function: makeInvocation( + nftContract, + "transfer", + nftContract.address().toScVal(), + "2", + ), + subInvocations: [], + }), + new xdr.SorobanAuthorizedInvocation({ + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeCreateContractHostFn( + new xdr.CreateContractArgs({ + contractIdPreimage: + xdr.ContractIdPreimage.contractIdPreimageFromAddress( + new xdr.ContractIdPreimageFromAddress({ + address: nftContract.address().toScAddress(), + salt: Buffer.alloc(32, 0), + }), + ), + executable: xdr.ContractExecutable.contractExecutableWasm( + Buffer.alloc(32, "\x20"), + ), + }), + ), + subInvocations: [], + }), + ]; + + const rootInvocation = new xdr.SorobanAuthorizedInvocation({ + function: makeInvocation(nftContract, "purchase", `SomeNft:${nftId}`, 7), + subInvocations: rootSubInvocations, + }); + + it("get invocation details for a single Soroban Authorized Invocation of 'invoke' type", () => { + const detailsList = getInvocationDetails(rootInvocation); + + const rootDetail = detailsList[0]; + + /* + rootDetails prints: + + { + fnName: 'purchase', + contractId: 'CDG44CP4LWYVRELBCICJYWUJ6B3NCKAOOIAX3MR5HGLFFK3ZWPSZJLMV', + args: [ + ChildUnion { + _switch: [ChildEnum], + _arm: 'str', + _armType: [String], + _value: 'SomeNft:GDASJXL2RYFJCRHXRZQ3ADPEXS5KVXKTOR3FCRCBFCQ77YZXDXMPV7D3' + }, + ChildUnion { + _switch: [ChildEnum], + _arm: 'u64', + _armType: [Function], + _value: [UnsignedHyper] + } + ], + type: 'invoke' + } + */ + + expect(rootDetail.type).toBe("invoke"); + expect(rootDetail.fnName).toBe("purchase"); + expect(rootDetail.contractId).toBe(nftContract.contractId()); + expect(rootDetail.args.length).toBe(2); + expect(scValByType(rootDetail.args[0])).toBe(`SomeNft:${nftId}`); + expect(Number(scValByType(rootDetail.args[1]))).toBe(7); + }); + + it("get invocation details for the main invocation and its immediate sub invocations", () => { + const detailsList = getInvocationDetails(rootInvocation); + + expect(detailsList.length).toBe(5); + + const [rootDetail, subDetail1, subDetail2, subDetail3, subDetail4] = + detailsList; + + /* + detailsList prints: + + [ + { + fnName: 'purchase', + contractId: 'CDG44CP4LWYVRELBCICJYWUJ6B3NCKAOOIAX3MR5HGLFFK3ZWPSZJLMV', + args: [ [ChildUnion], [ChildUnion] ], + type: 'invoke' + }, + { + type: 'sac', + asset: 'TEST:GDASJXL2RYFJCRHXRZQ3ADPEXS5KVXKTOR3FCRCBFCQ77YZXDXMPV7D3' + }, + { + fnName: 'swap', + contractId: 'CAY4K3IKHRPQUZARUFQ2QB7UZAIS2CGAMA3OUE7UUWDX3FDGE7DTOYF7', + args: [ [ChildUnion], [ChildUnion], [ChildUnion], [ChildUnion] ], + type: 'invoke' + }, + { + fnName: 'transfer', + contractId: 'CDG44CP4LWYVRELBCICJYWUJ6B3NCKAOOIAX3MR5HGLFFK3ZWPSZJLMV', + args: [ [ChildUnion], [ChildUnion] ], + type: 'invoke' + }, + { + type: 'wasm', + salt: '0000000000000000000000000000000000000000000000000000000000000000', + hash: '2020202020202020202020202020202020202020202020202020202020202020', + address: 'CDG44CP4LWYVRELBCICJYWUJ6B3NCKAOOIAX3MR5HGLFFK3ZWPSZJLMV' + } + ] + */ + + expect(rootDetail.type).toBe("invoke"); + expect(rootDetail.fnName).toBe("purchase"); + expect(rootDetail.contractId).toBe(nftContract.contractId()); + expect(rootDetail.args.length).toBe(2); + expect(scValByType(rootDetail.args[0])).toBe(`SomeNft:${nftId}`); + expect(Number(scValByType(rootDetail.args[1]))).toBe(7); + expect(rootDetail.salt).toBeUndefined(); + expect(rootDetail.hash).toBeUndefined(); + expect(rootDetail.address).toBeUndefined(); + expect(rootDetail.asset).toBeUndefined(); + + expect(subDetail1.type).toBe("sac"); + expect(subDetail1.fnName).toBeUndefined(); + expect(subDetail1.contractId).toBeUndefined(); + expect(subDetail1.args).toBeUndefined(); + expect(subDetail1.salt).toBeUndefined(); + expect(subDetail1.hash).toBeUndefined(); + expect(subDetail1.address).toBeUndefined(); + expect(subDetail1.asset).toBe(`TEST:${nftId}`); + + expect(subDetail2.type).toBe("invoke"); + expect(subDetail2.fnName).toBe("swap"); + expect(subDetail2.contractId).toBe(swapContract.contractId()); + expect(subDetail2.args.length).toBe(4); + expect(scValByType(subDetail2.args[0])).toBe("native"); + expect(scValByType(subDetail2.args[1])).toBe(`USDC:${usdcId}`); + expect(scValByType(subDetail2.args[2])).toBe(invoker); + expect(scValByType(subDetail2.args[3])).toBe(dest); + expect(subDetail2.salt).toBeUndefined(); + expect(subDetail2.hash).toBeUndefined(); + expect(subDetail2.address).toBeUndefined(); + expect(subDetail2.asset).toBeUndefined(); + + expect(subDetail3.type).toBe("invoke"); + expect(subDetail3.fnName).toBe("transfer"); + expect(subDetail3.contractId).toBe(nftContract.contractId()); + expect(subDetail3.args.length).toBe(2); + expect(scValByType(subDetail3.args[0])).toBe( + scValByType(nftContract.address().toScVal()), + ); + expect(scValByType(subDetail3.args[1])).toBe("2"); + expect(subDetail3.salt).toBeUndefined(); + expect(subDetail3.hash).toBeUndefined(); + expect(subDetail3.address).toBeUndefined(); + expect(subDetail3.asset).toBeUndefined(); + + expect(subDetail4.type).toBe("wasm"); + expect(subDetail4.fnName).toBeUndefined(); + expect(subDetail4.contractId).toBeUndefined(); + expect(subDetail4.args).toBeUndefined(); + expect(subDetail4.salt).toBe(Buffer.alloc(32, 0).toString("hex")); + expect(subDetail4.hash).toBe(Buffer.alloc(32, "\x20").toString("hex")); + expect(subDetail4.address).toBe( + Address.fromScAddress(nftContract.address().toScAddress()).toString(), + ); + expect(subDetail4.asset).toBeUndefined(); + }); +}); diff --git a/@stellar/typescript-wallet-sdk-soroban/test/tsconfig.json b/@stellar/typescript-wallet-sdk-soroban/test/tsconfig.json new file mode 100644 index 0000000..943c1f7 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "baseUrl": "./", + "outDir": "lib", + "declaration": true, + "declarationDir": "lib" + }, + "include": ["./"] +} diff --git a/@stellar/typescript-wallet-sdk-soroban/test/utils/index.ts b/@stellar/typescript-wallet-sdk-soroban/test/utils/index.ts new file mode 100644 index 0000000..948e11d --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/test/utils/index.ts @@ -0,0 +1,38 @@ +import { + Contract, + Keypair, + StrKey, + hash, + nativeToScVal, + xdr, +} from "@stellar/stellar-sdk"; + +// Returns random public key +export const randomKey = (): string => { + return Keypair.random().publicKey(); +}; + +// Creates a 'n' number of contracts with random ids +export const randomContracts = (n: number) => { + return Array.from(Array(n).keys()).map(() => { + // ezpz method to generate random contract IDs + const buf = hash(Buffer.from(randomKey())); + const contractId = StrKey.encodeContract(buf); + return new Contract(contractId); + }); +}; + +// Returns a SorobanAuthorizedFunction invocation with given args +export const makeInvocation = ( + contract: Contract, + name: string, + ...args: any[] +): xdr.SorobanAuthorizedFunction => { + return xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( + new xdr.InvokeContractArgs({ + contractAddress: contract.address().toScAddress(), + functionName: name, + args: args.map((arg) => nativeToScVal(arg)), + }), + ); +}; diff --git a/@stellar/typescript-wallet-sdk-soroban/tsconfig.json b/@stellar/typescript-wallet-sdk-soroban/tsconfig.json new file mode 100644 index 0000000..241b525 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "baseUrl": "src/", + "outDir": "lib", + "declaration": true, + "declarationDir": "lib" + }, + "include": ["src"] +} diff --git a/@stellar/typescript-wallet-sdk-soroban/webpack.config.js b/@stellar/typescript-wallet-sdk-soroban/webpack.config.js new file mode 100644 index 0000000..c624301 --- /dev/null +++ b/@stellar/typescript-wallet-sdk-soroban/webpack.config.js @@ -0,0 +1,51 @@ +const path = require("path"); +const webpack = require("webpack"); + +module.exports = (env = { NODE: false }) => { + const isBrowser = !env.NODE; + + return { + mode: "development", + entry: "./src/index.ts", + devtool: "source-map", + module: { + rules: [ + { + test: /\.ts$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".js", ".json", ".ts"], + fallback: isBrowser + ? { + crypto: require.resolve("crypto-browserify"), + http: require.resolve("stream-http"), + https: require.resolve("https-browserify"), + stream: require.resolve("stream-browserify"), + url: require.resolve("url"), + util: require.resolve("util"), + vm: require.resolve("vm-browserify"), + "process/browser": require.resolve("process/browser"), + } + : {}, + }, + output: { + library: "WalletSDK", + libraryTarget: "umd", + globalObject: "this", + filename: `bundle${isBrowser ? "_browser" : ""}.js`, + path: path.resolve(__dirname, "lib"), + }, + target: isBrowser ? "web" : "node", + plugins: isBrowser + ? [ + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ] + : [], + }; +}; diff --git a/@stellar/typescript-wallet-sdk/CHANGELOG.MD b/@stellar/typescript-wallet-sdk/CHANGELOG.MD new file mode 100644 index 0000000..bd75d5b --- /dev/null +++ b/@stellar/typescript-wallet-sdk/CHANGELOG.MD @@ -0,0 +1,88 @@ +# Release notes - Typescript Wallet SDK - 1.5.0 + +# Added +* AuthHeaderSigner to Authentication Flow +* End to end tests for testing browser build +* Beta builds on merges to develop branch + +# Release notes - Typescript Wallet SDK - 1.4.0 + +### Added +* SEP-10 sign challenge transaction helper +* Anchor platform integration tests +* Change project structure to monorepo +* Helper for parsing AnchorTransaction + +### Fixed +* Fix stellar-sdk imports + +# Release notes - Typescript Wallet SDK - 1.3.1 + +### Added +* Upgraded stellar-sdk to 11.1.0 + +# Release notes - Typescript Wallet SDK - 1.3.0 + +### Added +* Type aliases +* Account setup and recovery using SEP-30 +* Customer / SEP-12 code +* SEP-6 deposit and withdrawal +* Exchange endpoints +* Recovery integration tests +* Watcher and polling for SEP-6 +* AuthToken class +* Account merge and premade assets +* SEP-38 info, price, and prices +* SEP-38 Quote + +### Fixed +* Some small fixes to recovery code + +# Release notes - Typescript Wallet SDK - 1.2.1 + +### Fixed +* Better handle axios errors + +# Release notes - Typescript Wallet SDK - 1.2.0 + +### Added +* Sponosring transactions +* Account modification functions +* Default domain signer and default client_domain +* Path payment and swap +* Sep24 example code + +### Fixed +* Add build for both node and browser + +# Release notes - Typescript Wallet SDK - 1.1.3 + +### Fixed +* Check if withdraw memo is hash type + +### Added +* Upgrading stellar-sdk veresion for protocol 20 + + +# Release notes - Typescript Wallet SDK - 1.1.2 + +### Fixed +* Fix watcher stopping +* Only emit txn if status changed + + +# Release notes - Typescript Wallet SDK - 1.1.0 + +### Added +* Submitting a transaction +* Manage non-XLM trustlines +* Importing and signing arbitrary transactions given an XDR +* Horizon getInfo and gitHistory functions +* Helper method for creating a keypair from random bytes +* Exporting classes +* Transfer withdrawal method +* Fee bump transaction +* Building function to submitWithFeeIncrease + + diff --git a/@stellar/typescript-wallet-sdk/examples/sep24/sep24.ts b/@stellar/typescript-wallet-sdk/examples/sep24/sep24.ts index 6a115da..59be0b3 100644 --- a/@stellar/typescript-wallet-sdk/examples/sep24/sep24.ts +++ b/@stellar/typescript-wallet-sdk/examples/sep24/sep24.ts @@ -9,6 +9,7 @@ import { Types, IssuedAssetId, DefaultSigner, + Wallet, } from "../../src"; import { Memo, @@ -34,7 +35,7 @@ const clientSecret = process.env.CLIENT_SECRET; // Running example -let wallet; +let wallet: Wallet; if (runMainnet === "true") { console.log("Warning: you are running this script on the public network."); wallet = walletSdk.Wallet.MainNet(); @@ -123,7 +124,6 @@ export let depositDone = false; export const runDepositWatcher = (anchor: Anchor) => { console.log("\nstarting watcher ..."); - const stop: Types.WatcherStopFunction; const onMessage = (m: Types.AnchorTransaction) => { console.log({ m }); if (m.status === Types.TransactionStatus.completed) { @@ -138,7 +138,7 @@ export const runDepositWatcher = (anchor: Anchor) => { }; const watcher = anchor.sep24().watcher(); - const resp = watcher.watchAllTransactions({ + const { stop } = watcher.watchAllTransactions({ authToken: authToken, assetCode: asset.code, onMessage, @@ -146,8 +146,6 @@ export const runDepositWatcher = (anchor: Anchor) => { timeout: 5000, lang: "en-US", }); - - stop = resp.stop; }; // Create Withdrawal @@ -188,7 +186,6 @@ const sendWithdrawalTransaction = async (withdrawalTxn, kp) => { export const runWithdrawWatcher = (anchor, kp) => { console.log("\nstarting watcher ..."); - const stop; const onMessage = (m) => { console.log({ m }); @@ -208,7 +205,7 @@ export const runWithdrawWatcher = (anchor, kp) => { }; const watcher = anchor.sep24().watcher(); - const resp = watcher.watchAllTransactions({ + const { stop } = watcher.watchAllTransactions({ authToken: authToken, assetCode: asset.code, onMessage, @@ -216,8 +213,6 @@ export const runWithdrawWatcher = (anchor, kp) => { timeout: 5000, lang: "en-US", }); - - stop = resp.stop; }; const walletSigner = DefaultSigner; diff --git a/@stellar/typescript-wallet-sdk/jest.e2e.config.js b/@stellar/typescript-wallet-sdk/jest.e2e.config.js new file mode 100644 index 0000000..ea8b056 --- /dev/null +++ b/@stellar/typescript-wallet-sdk/jest.e2e.config.js @@ -0,0 +1,9 @@ +module.exports = { + rootDir: "./", + preset: "ts-jest", + transform: { + "^.+\\.(ts|tsx)?$": "ts-jest", + "^.+\\.(js|jsx)$": "babel-jest", + }, + testMatch: ["**/e2e/*.test.ts"], +}; diff --git a/@stellar/typescript-wallet-sdk/package.json b/@stellar/typescript-wallet-sdk/package.json index 7162c9b..d9b7a36 100644 --- a/@stellar/typescript-wallet-sdk/package.json +++ b/@stellar/typescript-wallet-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk", - "version": "1.4.1", + "version": "1.5.0", "engines": { "node": ">=18" }, @@ -29,6 +29,7 @@ "jest": "^29.4.1", "lint-staged": "^14.0.1", "npm-run-all": "^4.1.5", + "playwright": "^1.43.1", "prettier": "^2.0.5", "pretty-quick": "^2.0.1", "process": "^0.11.10", @@ -45,11 +46,14 @@ "dependencies": { "@stellar/stellar-sdk": "^11.1.0", "axios": "^1.4.0", + "base64url": "^3.0.1", "https-browserify": "^1.0.0", "jws": "^4.0.0", "lodash": "^4.17.21", "query-string": "^7.1.3", "stream-http": "^3.2.0", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1", "url": "^0.11.0", "util": "^0.12.5", "utility-types": "^3.10.0", @@ -58,6 +62,7 @@ "scripts": { "test": "jest --watchAll", "test:ci": "jest --ci", + "test:e2e:ci": "jest --config jest.e2e.config.js --ci", "test:recovery:ci": "jest --config jest.integration.config.js recovery.test.ts --ci", "test:anchorplatform:ci": "yarn jest --config jest.integration.config.js anchorplatform.test.ts --ci", "build:web": "webpack --config webpack.config.js", diff --git a/@stellar/typescript-wallet-sdk/src/index.ts b/@stellar/typescript-wallet-sdk/src/index.ts index 74d009e..cb6050b 100644 --- a/@stellar/typescript-wallet-sdk/src/index.ts +++ b/@stellar/typescript-wallet-sdk/src/index.ts @@ -17,6 +17,11 @@ export { Anchor } from "./walletSdk/Anchor"; export { Sep24 } from "./walletSdk/Anchor/Sep24"; export { IssuedAssetId, NativeAssetId, FiatAssetId } from "./walletSdk/Asset"; export { Sep10, WalletSigner, DefaultSigner } from "./walletSdk/Auth"; +export { + AuthHeaderSigner, + DefaultAuthHeaderSigner, + DomainAuthHeaderSigner, +} from "./walletSdk/Auth/AuthHeaderSigner"; export { AccountKeypair, PublicKeypair, diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts index 806dfc0..420a87c 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts @@ -1,7 +1,7 @@ import { AxiosInstance } from "axios"; import { StellarToml } from "@stellar/stellar-sdk"; -import { Config } from "walletSdk"; +import { Config } from "../"; import { Sep10 } from "../Auth"; import { Sep12 } from "../Customer"; import { diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/AuthHeaderSigner.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/AuthHeaderSigner.ts new file mode 100644 index 0000000..25185a6 --- /dev/null +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/AuthHeaderSigner.ts @@ -0,0 +1,162 @@ +import { AxiosInstance } from "axios"; +import { StrKey } from "@stellar/stellar-sdk"; +import nacl from "tweetnacl"; +import naclUtil from "tweetnacl-util"; +import base64url from "base64url"; + +import { SigningKeypair } from "../Horizon/Account"; +import { DefaultClient } from "../"; +import { AuthHeaderClaims, AuthHeaderCreateTokenParams } from "../Types"; +import { + AuthHeaderSigningKeypairRequiredError, + AuthHeaderClientDomainRequiredError, +} from "../Exceptions"; + +export interface AuthHeaderSigner { + createToken({ + claims, + clientDomain, + issuer, + }: AuthHeaderCreateTokenParams): Promise; +} + +/** + * Signer for signing JWT for GET /Auth with a custodial private key + * + * @class + */ +export class DefaultAuthHeaderSigner implements AuthHeaderSigner { + expiration: number; + + constructor(expiration: number = 900) { + this.expiration = expiration; + } + + /** + * Create a signed JWT for the auth header + * @constructor + * @param {AuthHeaderCreateTokenParams} params - The create token parameters + * @param {AuthHeaderClaims} params.claims - the data to be signed in the JWT + * @param {string} [params.clientDomain] - the client domain hosting SEP-1 toml + * @param {AccountKeypair} [params.issuer] - the account signing the JWT + * @returns {Promise} The signed JWT + */ + // eslint-disable-next-line @typescript-eslint/require-await + async createToken({ + claims, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + clientDomain, + issuer, + }: AuthHeaderCreateTokenParams): Promise { + if (!(issuer instanceof SigningKeypair)) { + throw new AuthHeaderSigningKeypairRequiredError(); + } + + const issuedAt = claims.iat || Math.floor(Date.now() / 1000); + const timeExp = + claims.exp || Math.floor(Date.now() / 1000) + this.expiration; + + // turn stellar kp into nacl kp for creating JWT + const rawSeed = StrKey.decodeEd25519SecretSeed(issuer.secretKey); + const naclKP = nacl.sign.keyPair.fromSeed(rawSeed); + + // encode JWT message + const header = { alg: "EdDSA" }; + const encodedHeader = base64url(JSON.stringify(header)); + const encodedPayload = base64url( + JSON.stringify({ ...claims, exp: timeExp, iat: issuedAt }), + ); + + // sign JWT and create signature + const signature = nacl.sign.detached( + naclUtil.decodeUTF8(`${encodedHeader}.${encodedPayload}`), + naclKP.secretKey, + ); + const encodedSignature = base64url(Buffer.from(signature)); + + const jwt = `${encodedHeader}.${encodedPayload}.${encodedSignature}`; + return jwt; + } +} + +/** + * Signer for signing JWT for GET /Auth using a remote server to sign. + * + * @class + */ +export class DomainAuthHeaderSigner implements AuthHeaderSigner { + signerUrl: string; + expiration: number; + httpClient: AxiosInstance; + + constructor( + signerUrl: string, + expiration: number = 900, + httpClient?: AxiosInstance, + ) { + this.signerUrl = signerUrl; + this.expiration = expiration; + this.httpClient = httpClient || DefaultClient; + } + + /** + * Create a signed JWT for the auth header by using a remote server to sign the JWT + * @constructor + * @param {AuthHeaderCreateTokenParams} params - The create token parameters + * @param {AuthHeaderClaims} params.claims - the data to be signed in the JWT + * @param {string} [params.clientDomain] - the client domain hosting SEP-1 toml + * @param {AccountKeypair} [params.issuer] - unused, will not be used to sign + * @returns {Promise} The signed JWT + */ + async createToken({ + claims, + clientDomain, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + issuer, + }: AuthHeaderCreateTokenParams): Promise { + if (!clientDomain) { + throw new AuthHeaderClientDomainRequiredError(); + } + + const issuedAt = Math.floor(Date.now() / 1000); + const expiration = Math.floor(Date.now() / 1000) + this.expiration; + + return await this.signTokenRemote({ + claims, + clientDomain, + expiration, + issuedAt, + }); + } + + /** + * Sign JWT by calling a remote server + * @constructor + * @param {SignTokenRemoteParams} params - the sign token params + * @param {AuthHeaderClaims} params.claims - the data to be signed in the JWT + * @param {string} params.clientDomain - the client domain hosting SEP-1 toml + * @param {number} params.expiration - when the token should expire + * @param {number} params.issuedAt - when the token was created + * @returns {Promise} The signed JWT + */ + async signTokenRemote({ + claims, + clientDomain, + expiration, + issuedAt, + }: { + claims: AuthHeaderClaims; + clientDomain: string; + expiration: number; + issuedAt: number; + }): Promise { + const resp = await this.httpClient.post(this.signerUrl, { + clientDomain, + expiration, + issuedAt, + ...claims, + }); + + return resp.data.token; + } +} diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts index c7d722c..8385c3d 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts @@ -2,7 +2,7 @@ import { AxiosInstance } from "axios"; import { TransactionBuilder, Transaction } from "@stellar/stellar-sdk"; import { decode } from "jws"; -import { Config } from "walletSdk"; +import { Config } from "../"; import { InvalidMemoError, ClientDomainWithMemoError, @@ -17,7 +17,10 @@ import { ChallengeParams, ChallengeResponse, SignParams, + AuthHeaderClaims, } from "../Types"; +import { AccountKeypair } from "../Horizon/Account"; +import { AuthHeaderSigner } from "./AuthHeaderSigner"; export { WalletSigner, DefaultSigner } from "./WalletSigner"; @@ -71,11 +74,13 @@ export class Sep10 { walletSigner, memoId, clientDomain, + authHeaderSigner, }: AuthenticateParams): Promise { const challengeResponse = await this.challenge({ accountKp, memoId, clientDomain: clientDomain || this.cfg.app.defaultClientDomain, + authHeaderSigner, }); const signedTransaction = await this.sign({ accountKp, @@ -90,6 +95,7 @@ export class Sep10 { accountKp, memoId, clientDomain, + authHeaderSigner, }: ChallengeParams): Promise { if (memoId && parseInt(memoId) < 0) { throw new InvalidMemoError(); @@ -104,8 +110,29 @@ export class Sep10 { }${clientDomain ? `&client_domain=${clientDomain}` : ""}${ this.homeDomain ? `&home_domain=${this.homeDomain}` : "" }`; + + const claims = { + account: accountKp.publicKey, + home_domain: this.homeDomain, + memo: memoId, + client_domain: clientDomain, + web_auth_endpoint: this.webAuthEndpoint, + }; + + const token = await createAuthSignToken( + accountKp, + claims, + clientDomain, + authHeaderSigner, + ); + + let headers = {}; + if (token) { + headers = { Authorization: `Bearer ${token}` }; + } + try { - const resp = await this.httpClient.get(url); + const resp = await this.httpClient.get(url, { headers }); const challengeResponse: ChallengeResponse = resp.data; return challengeResponse; } catch (e) { @@ -165,3 +192,22 @@ const validateToken = (token: string) => { throw new ExpiredTokenError(parsedToken.expiresAt); } }; + +const createAuthSignToken = async ( + account: AccountKeypair, + claims: AuthHeaderClaims, + clientDomain?: string, + authHeaderSigner?: AuthHeaderSigner, +) => { + if (!authHeaderSigner) { + return null; + } + + const issuer = clientDomain ? null : account; + + return authHeaderSigner.createToken({ + claims, + clientDomain, + issuer, + }); +}; diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts index 126e7d3..eb8aceb 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts @@ -305,3 +305,22 @@ export class InvalidJsonError extends Error { Object.setPrototypeOf(this, InvalidJsonError.prototype); } } + +export class AuthHeaderSigningKeypairRequiredError extends Error { + constructor() { + super("Must be SigningKeypair to sign auth header"); + Object.setPrototypeOf( + this, + AuthHeaderSigningKeypairRequiredError.prototype, + ); + } +} + +export class AuthHeaderClientDomainRequiredError extends Error { + constructor() { + super( + "This class should only be used for remote signing. For local signing use DefaultAuthHeaderSigner.", + ); + Object.setPrototypeOf(this, AuthHeaderClientDomainRequiredError.prototype); + } +} diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/AccountService.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/AccountService.ts index 1a134bf..e567e30 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/AccountService.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/AccountService.ts @@ -1,6 +1,6 @@ import { Keypair, Networks, Horizon } from "@stellar/stellar-sdk"; -import { Config } from "walletSdk"; +import { Config } from "../"; import { SigningKeypair } from "./Account"; import { HORIZON_LIMIT_DEFAULT, diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Stellar.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Stellar.ts index c5f164e..b4b5b54 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Stellar.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Stellar.ts @@ -7,7 +7,7 @@ import { } from "@stellar/stellar-sdk"; import axios from "axios"; -import { Config } from "walletSdk"; +import { Config } from "../"; import { AccountService } from "./AccountService"; import { TransactionBuilder } from "./Transaction/TransactionBuilder"; import { diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Transaction/TransactionBuilder.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Transaction/TransactionBuilder.ts index 1229cbe..8b3b359 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Transaction/TransactionBuilder.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Horizon/Transaction/TransactionBuilder.ts @@ -8,7 +8,7 @@ import { xdr, } from "@stellar/stellar-sdk"; -import { Config } from "walletSdk"; +import { Config } from "../../"; import { AccountKeypair } from "../Account"; import { InsufficientStartingBalanceError, diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/AccountRecover.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/AccountRecover.ts index f8f299f..d6aea4d 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/AccountRecover.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/AccountRecover.ts @@ -7,7 +7,7 @@ import { RecoveryServerMap, RecoveryServerSigning, RecoveryServerSigningMap, -} from "walletSdk/Types"; +} from "../Types"; import { LostSignerKeyNotFound, NoDeviceKeyForAccountError, diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts index 26d04b8..5df8af1 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts @@ -1,7 +1,7 @@ import { AxiosInstance } from "axios"; import { Transaction } from "@stellar/stellar-sdk"; -import { Config } from "walletSdk"; +import { Config } from "../"; import { AccountSigner, AccountThreshold, @@ -16,7 +16,7 @@ import { RecoveryIdentityMap, RecoveryServerKey, RecoveryServerMap, -} from "walletSdk/Types"; +} from "../Types"; import { AccountRecover } from "./AccountRecover"; import { Sep10 } from "../Auth"; import { diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts index ee494da..f88099a 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts @@ -3,12 +3,14 @@ import { decode } from "jws"; import { WalletSigner } from "../Auth/WalletSigner"; import { AccountKeypair, SigningKeypair } from "../Horizon/Account"; +import { AuthHeaderSigner } from "../Auth/AuthHeaderSigner"; export type AuthenticateParams = { accountKp: AccountKeypair; walletSigner?: WalletSigner; memoId?: string; clientDomain?: string; + authHeaderSigner?: AuthHeaderSigner; }; export class AuthToken { @@ -49,6 +51,7 @@ export type ChallengeParams = { accountKp: AccountKeypair; memoId?: string; clientDomain?: string; + authHeaderSigner?: AuthHeaderSigner; }; export type XdrEncodedTransaction = string; @@ -91,3 +94,19 @@ export type SignChallengeTxnResponse = { transaction: XdrEncodedTransaction; networkPassphrase: NetworkPassphrase; }; + +export type AuthHeaderClaims = { + account: string; + home_domain: string; + web_auth_endpoint: string; + memo?: string; + client_domain?: string; + exp?: number; + iat?: number; +}; + +export type AuthHeaderCreateTokenParams = { + claims: AuthHeaderClaims; + clientDomain?: string; + issuer?: AccountKeypair; +}; diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/horizon.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/horizon.ts index 4d837ad..89fee61 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/horizon.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/horizon.ts @@ -1,6 +1,6 @@ import { Memo, Horizon, Transaction } from "@stellar/stellar-sdk"; import { AccountKeypair } from "../Horizon/Account"; -import { SponsoringBuilder, TransactionBuilder } from "walletSdk/Horizon"; +import { SponsoringBuilder, TransactionBuilder } from "../Horizon"; import { StellarAssetId } from "../Asset"; export enum NETWORK_URLS { diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/index.ts index 31dfd9c..bb6fd43 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/index.ts @@ -1,6 +1,6 @@ import { RawAxiosRequestHeaders } from "axios"; import { Networks } from "@stellar/stellar-sdk"; -import { ApplicationConfiguration, StellarConfiguration } from "walletSdk"; +import { ApplicationConfiguration, StellarConfiguration } from "../"; import { RecoveryServerMap } from "./recovery"; // Export types from root walletSdk/index.ts diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts index 1f0102e..119f254 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts @@ -1,7 +1,7 @@ import { Transaction } from "@stellar/stellar-sdk"; -import { WalletSigner } from "walletSdk/Auth"; -import { AccountKeypair, PublicKeypair } from "walletSdk/Horizon"; +import { WalletSigner } from "../Auth"; +import { AccountKeypair, PublicKeypair } from "../Horizon"; import { AuthToken } from "./auth"; import { CommonBuilder } from "./horizon"; diff --git a/@stellar/typescript-wallet-sdk/test/README.md b/@stellar/typescript-wallet-sdk/test/README.md deleted file mode 100644 index 864bbed..0000000 --- a/@stellar/typescript-wallet-sdk/test/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Recovery Integration Tests - -## How it works - -The recovery integration tests run different recovery scenarios against recovery -signer and webauth servers. 2 recovery signer and 2 webauth servers are started -in a docker-compose file (see test/docker/docker-compose.yml), to simulate a -wallet interacting with 2 separate recovery servers. - -## To run tests locally: - -``` -// start servers using docker -$ docker-compose -f test/docker/docker-compose.yml up - -// run tests -$ yarn test:integration:ci -``` diff --git a/@stellar/typescript-wallet-sdk/test/e2e/README.md b/@stellar/typescript-wallet-sdk/test/e2e/README.md new file mode 100644 index 0000000..19776e8 --- /dev/null +++ b/@stellar/typescript-wallet-sdk/test/e2e/README.md @@ -0,0 +1,11 @@ +# How it works + +## browser.test.ts + +This test uses playwright to load the browser bundle file into a browser +environment and run its code. If there is a bug in how its built, +window.WalletSDK will be undefined. + +### To run + +$ yarn build $ yarn test browser.test.ts diff --git a/@stellar/typescript-wallet-sdk/test/e2e/browser.test.ts b/@stellar/typescript-wallet-sdk/test/e2e/browser.test.ts new file mode 100644 index 0000000..62fa754 --- /dev/null +++ b/@stellar/typescript-wallet-sdk/test/e2e/browser.test.ts @@ -0,0 +1,52 @@ +import { chromium, webkit } from "playwright"; + +describe("Test browser build", () => { + const browsers = [ + { name: "Chrome", instance: chromium }, + { name: "Safari", instance: webkit }, + ]; + + for (const b of browsers) { + it( + "works on " + b.name, + async () => { + await (async () => { + const browser = await b.instance.launch(); + const page = await browser.newPage(); + + await page.goto("https://stellar.org"); + + await page.addScriptTag({ + path: "./lib/bundle_browser.js", + }); + + // Use the Stellar SDK in the website's context + const result = await page.evaluate(() => { + let kp; + try { + const wal = (window as any).WalletSDK.Wallet.TestNet(); + const account = wal.stellar().account(); + + kp = account.createKeypair(); + } catch (e) { + return { success: false }; + } + + return { + publicKey: kp.publicKey, + secretKey: kp.secretKey, + success: true, + }; + }); + + expect(result.publicKey).toBeTruthy(); + expect(result.secretKey).toBeTruthy(); + expect(result.success).toBeTruthy(); + + await browser.close(); + })(); + }, + 15000, + ); + } +}); diff --git a/@stellar/typescript-wallet-sdk/test/integration/README.md b/@stellar/typescript-wallet-sdk/test/integration/README.md new file mode 100644 index 0000000..fcf78d0 --- /dev/null +++ b/@stellar/typescript-wallet-sdk/test/integration/README.md @@ -0,0 +1,38 @@ +# Recovery Integration Tests + +## How it works + +The recovery integration tests run different recovery scenarios against recovery +signer and webauth servers. 2 recovery signer and 2 webauth servers are started +in a docker-compose file (see test/docker/docker-compose.yml), to simulate a +wallet interacting with 2 separate recovery servers. + +## To run tests locally: + +``` +// start servers using docker +$ docker-compose -f @stellar/typescript-wallet-sdk/test/docker/docker-compose.yml up + +// run tests +$ yarn test:recovery:ci +``` + +# AnchorPlatform Integration Tests + +## How it works + +This test works similar to the recovery integration tests. It spins up an +anchorplatform image from the (java anchor sdk +repo)[https://github.com/stellar/java-stellar-anchor-sdk] and runs tests +against. + +## To run tests locally: + +- follow the steps defined in the + (.github/workflows/integration.anchorPlatformTest.yml)[https://github.com/stellar/typescript-wallet-sdk/blob/main/.github/workflows/integration.anchorPlatformTest.yml] + file locally + +1. Clone the java-stellar-anchor-sdk repo locally +2. Run the docker build command +3. Run the docker command +4. Run the anchorPlatform tests from this repo diff --git a/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts b/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts index 49ce697..6715f5d 100644 --- a/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts +++ b/@stellar/typescript-wallet-sdk/test/integration/anchorplatform.test.ts @@ -1,5 +1,6 @@ import { Wallet } from "../../src"; import { IssuedAssetId } from "../../src/walletSdk/Asset"; +import { DefaultAuthHeaderSigner } from "../../src/walletSdk/Auth/AuthHeaderSigner"; let wallet; let stellar; @@ -23,6 +24,14 @@ describe("Anchor Platform Integration Tests", () => { expect(authToken.token).toBeTruthy(); }); + it("using DefaultAuthHeaderSigner should work", async () => { + const auth = await anchor.sep10(); + + const authHeaderSigner = new DefaultAuthHeaderSigner(); + const authToken = await auth.authenticate({ accountKp, authHeaderSigner }); + expect(authToken.token).toBeTruthy(); + }); + it("SEP-12 KYC and SEP-6 should work", async () => { const auth = await anchor.sep10(); const authToken = await auth.authenticate({ accountKp }); diff --git a/@stellar/typescript-wallet-sdk/test/server.test.ts b/@stellar/typescript-wallet-sdk/test/server.test.ts index f009a56..69bf520 100644 --- a/@stellar/typescript-wallet-sdk/test/server.test.ts +++ b/@stellar/typescript-wallet-sdk/test/server.test.ts @@ -68,4 +68,17 @@ describe("Server helpers", () => { parsed = Server.parseAnchorTransaction(withdrawJson); expect(parsed.kind).toBe("withdrawal"); }); + it("should parse moneygram JSON transactions", () => { + const d1 = `{"id":"19489958-c0c4-4090-a272-a51fc851f524","kind":"deposit","status":"incomplete","amount_in":"3.00","amount_in_asset":"USD","amount_out":"3.00","amount_out_asset":"USD","amount_fee":"0.00","amount_fee_asset":"USD","started_at":"2024-03-28T16:20:09Z","stellar_transaction_id":"","refunded":false,"from":"","to":""}`; + let parsed = Server.parseAnchorTransaction(d1); + expect(parsed.kind).toBe("deposit"); + + const w1 = `{"withdraw_anchor_account":"GAYF33NNNMI2Z6VNRFXQ64D4E4SF77PM46NW3ZUZEEU5X7FCHAZCMHKU","withdraw_memo":"639496083328800102","withdraw_memo_type":"id","id":"d64c5d56-de6d-492e-95dd-412fb86c1c14","kind":"withdrawal","status":"pending_user_transfer_start","more_info_url":"https://extstellar.moneygram.com/transaction-status?transaction_id\u003dd64c5d56-de6d-492e-95dd-412fb86c1c14\u0026token\u003deyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJkNjRjNWQ1Ni1kZTZkLTQ5MmUtOTVkZC00MTJmYjg2YzFjMTQiLCJpc3MiOiJtb3JlSW5mb1VybCIsInN1YiI6IkdBWkRVRlIyTDQ3S0hBS1g0V1NVWDNJUFo3NDYyVDVFNzNNQVpIWE9XT0NFQlBBUVlIVDdFNjJGIiwiaWF0IjoxNzExNjQyNzU0LCJleHAiOjE3MTE3MjkxNTQsImNsaWVudF9kb21haW4iOiJhcGktZGV2LnZpYnJhbnRhcHAuY29tIn0.CNjnMzXYA9aSU0ZA9Gd-P5bDWmpnaoAen8SnGz6PlHQ\u0026lang\u003den-US\u0026refNumber\u003d89445520","amount_in":"3.0","amount_in_asset":"USDC","amount_out":"4.02","amount_out_asset":"CAD","amount_fee":"0.0","amount_fee_asset":"USDC","started_at":"2024-03-28T16:18:02Z","stellar_transaction_id":"","external_transaction_id":"89445520","refunded":false,"from":"GAZDUFR2L47KHAKX4WSUX3IPZ7462T5E73MAZHXOWOCEBPAQYHT7E62F","to":"GAYF33NNNMI2Z6VNRFXQ64D4E4SF77PM46NW3ZUZEEU5X7FCHAZCMHKU"}`; + parsed = Server.parseAnchorTransaction(w1); + expect(parsed.kind).toBe("withdrawal"); + + const w2 = `{"id":"d64c5d56-de6d-492e-95dd-412fb86c1c14","kind":"withdrawal","status":"incomplete","amount_in":"0","amount_out":"0","amount_fee":"0","started_at":"2024-03-28T16:18:02Z","stellar_transaction_id":"","refunded":false,"from":"GAZDUFR2L47KHAKX4WSUX3IPZ7462T5E73MAZHXOWOCEBPAQYHT7E62F"}`; + parsed = Server.parseAnchorTransaction(w2); + expect(parsed.kind).toBe("withdrawal"); + }); }); diff --git a/@stellar/typescript-wallet-sdk/test/wallet.test.ts b/@stellar/typescript-wallet-sdk/test/wallet.test.ts index c2f3ea7..1ace43f 100644 --- a/@stellar/typescript-wallet-sdk/test/wallet.test.ts +++ b/@stellar/typescript-wallet-sdk/test/wallet.test.ts @@ -20,6 +20,10 @@ import { WalletSigner, DefaultSigner, } from "../src/walletSdk/Auth/WalletSigner"; +import { + DefaultAuthHeaderSigner, + DomainAuthHeaderSigner, +} from "../src/walletSdk/Auth/AuthHeaderSigner"; import { SigningKeypair } from "../src/walletSdk/Horizon/Account"; import { Sep24 } from "../src/walletSdk/Anchor/Sep24"; import { DomainSigner } from "../src/walletSdk/Auth/WalletSigner"; @@ -1880,3 +1884,84 @@ describe("Http client", () => { expect(resp.data.transaction).toBeTruthy(); }); }); + +describe("AuthHeaderSigner", () => { + beforeAll(() => { + jest.resetAllMocks(); + }); + it("full sep-10 auth using header token should work", async () => { + const wallet = Wallet.TestNet(); + const accountKp = wallet.stellar().account().createKeypair(); + wallet.stellar().fundTestnetAccount(accountKp.publicKey); + + const anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" }); + const auth = await anchor.sep10(); + + const authHeaderSigner = new DefaultAuthHeaderSigner(); + const authToken = await auth.authenticate({ + accountKp, + authHeaderSigner, + }); + + expect(authToken).toBeTruthy(); + }, 15000); + + it("should match example implementation generated JWT", async () => { + const generatedAuthToken = + "eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjE3MTE2NDg0ODYsImV4cCI6MTcxMTY0OTM4NiwiYWNjb3VudCI6IkdDNlVDWFZUQU1ORzVKTE9NWkJTQ05ZWFZTTk5GSEwyM1NKUFlPT0ZKRTJBVllERFMyRkZUNDVDIiwiY2xpZW50X2RvbWFpbiI6ImV4YW1wbGUtd2FsbGV0LnN0ZWxsYXIub3JnIiwid2ViX2F1dGhfZW5kcG9pbnQiOiJodHRwczovL2V4YW1wbGUuY29tL3NlcDEwL2F1dGgifQ.UQt8FpUK-BlnFw35o8Ke4GDOoCrMe9ztEx4_TGQ06XhMgUbn_b7EMPMVLWJ8RRNgSk2dNhyGUgIbhKzKtWtBBw"; + const issuer = SigningKeypair.fromSecret( + "SCYVDFYEHNDNTB2UER2FCYSZAYQFAAZ6BDYXL3BWRQWNL327GZUXY7D7", + ); + + const claims = { + iat: 1711648486, + exp: 1711649386, + account: "GC6UCXVTAMNG5JLOMZBSCNYXVSNNFHL23SJPYOOFJE2AVYDDS2FFT45C", + client_domain: "example-wallet.stellar.org", + web_auth_endpoint: "https://example.com/sep10/auth", + }; + + const signer = new DefaultAuthHeaderSigner(); + const token = await signer.createToken({ + claims, + clientDomain: "", + issuer, + }); + + expect(token).toBe(generatedAuthToken); + }); + + it("DefaultAuthHeaderSigner should work", async () => { + const accountKp = SigningKeypair.fromSecret( + "SAFXVNFRZQAC66RUZ2IJKMSNQCPXTKXVRX356COUKJJKJXBSLRX43DEZ", + ); + + const signer = new DefaultAuthHeaderSigner(); + const token = await signer.createToken({ + claims: {}, + clientDomain: "test-domain", + issuer: accountKp, + }); + expect(token).toBeTruthy(); + }); + + it("DomainAuthHeaderSigner should work", async () => { + const accountKp = SigningKeypair.fromSecret( + "SAFXVNFRZQAC66RUZ2IJKMSNQCPXTKXVRX356COUKJJKJXBSLRX43DEZ", + ); + + const signer = new DomainAuthHeaderSigner("some-url.com"); + + const data = { account: "dummy-account" }; + + jest.spyOn(signer, "signTokenRemote").mockResolvedValue("success-token"); + + const token = await signer.createToken({ + authTokenData: data, + clientDomain: "test-domain", + issuer: accountKp, + }); + + expect(token).toBe("success-token"); + }); +}); diff --git a/@stellar/typescript-wallet-sdk/webpack.config.js b/@stellar/typescript-wallet-sdk/webpack.config.js index c624301..1067ba9 100644 --- a/@stellar/typescript-wallet-sdk/webpack.config.js +++ b/@stellar/typescript-wallet-sdk/webpack.config.js @@ -29,6 +29,7 @@ module.exports = (env = { NODE: false }) => { util: require.resolve("util"), vm: require.resolve("vm-browserify"), "process/browser": require.resolve("process/browser"), + buffer: require.resolve("buffer"), } : {}, }, @@ -45,6 +46,9 @@ module.exports = (env = { NODE: false }) => { new webpack.ProvidePlugin({ process: "process/browser", }), + new webpack.ProvidePlugin({ + Buffer: ["buffer", "Buffer"], + }), ] : [], }; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..81ef429 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# How to contribute + +Please read the +[Contribution Guide](https://github.com/stellar/docs/blob/master/CONTRIBUTING.md). + +Then please +[sign the Contributor License Agreement](https://docs.google.com/forms/d/1g7EF6PERciwn7zfmfke5Sir2n10yddGGSXyZsq98tVY/viewform?usp=send_form). + +# Releasing + +1. Update package.json version file in all submodules to new version + +- all submodules should use same versioning + +2. Update CHANGELOG.md in submodules that are updated +3. Commit changes +4. Trigger a release using Github action for each updated submodule + +## Npm Pipelines + +All npm pipelines can be found in .github/workflows + +1. npmPublishSdk.yml + +- publishes typescript-wallet-sdk to npm + +2. npmPublishSdkKM.yml + +- publishes typescript-wallet-sdk-km to npm + +3. npmPublishSdkKM.yml + +- publishes typescript-wallet-sdk-soroban to npm + +4. npmPublishBeta.yml + +- publishes a beta build of typescript-wallet-sdk on merges to the `develop` + branch diff --git a/README.md b/README.md new file mode 100644 index 0000000..47ee8ab --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Typescript Wallet SDK Monorepo + +Typescript Wallet SDK is a library that allows developers to build wallet +applications on the Stellar network faster. To jump right into how the main +typescript-wallet-sdk module works you can +[go here](./@stellar/typescript-wallet-sdk/README.md). + +## Yarn Workspaces + +This repo uses a yarn worspace to create a monorepo that includes the main +wallet sdk, and two modules for extending functionality: + +- [typescript-wallet-sdk](./@stellar/typescript-wallet-sdk/README.md) - the main + sdk for building wallets on stellar +- [typescript-wallet-sdk-km](./@stellar/typescript-wallet-sdk-km/README.md) - + functionality for key managing +- [typescript-wallet-sdk-soroban](./@stellar/typescript-wallet-sdk-soroban/README.md) - + functionality for smart contracts on stellar + +## Prerequisites + +You will need + +- Node (>=18): https://nodejs.org/en/download/ +- Yarn (v1.22.5 or newer): https://classic.yarnpkg.com/en/docs/install + +## Install and Build the Project + +``` +yarn install +yarn build +``` + +- this will install and build for all sub modules + +## Testing + +``` +yarn test +``` + +- this will run all jest unit tests for each submodule + +Some tests that are not ran as part of that suite (but run during ci/cd): + +- [integration tests](./@stellar/typescript-wallet-sdk/test/integration/README.md) +- [e2e tests](./@stellar/typescript-wallet-sdk/test/e2e/README.md) + +## Example code + +Example code can be found in the main sdk +[examples directory](./@stellar/typescript-wallet-sdk/examples) diff --git a/jest.config.js b/jest.config.js index 3197ac0..94b2794 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,7 @@ module.exports = { { displayName: "Wallet SDK", roots: ["./@stellar/typescript-wallet-sdk"], - testPathIgnorePatterns: ["/node_modules/", "/integration/"], + testPathIgnorePatterns: ["/node_modules/", "/integration/", "/e2e/"], ...commonConfigs, }, { @@ -18,5 +18,11 @@ module.exports = { testPathIgnorePatterns: ["/node_modules/"], ...commonConfigs, }, + { + displayName: "Wallet SDK Soroban", + roots: ["./@stellar/typescript-wallet-sdk-soroban"], + testPathIgnorePatterns: ["/node_modules/"], + ...commonConfigs, + }, ], }; diff --git a/package.json b/package.json index 1098ecd..12fef58 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,18 @@ "main": ".eslintrc.js", "workspaces": [ "@stellar/typescript-wallet-sdk", - "@stellar/typescript-wallet-sdk-km" + "@stellar/typescript-wallet-sdk-km", + "@stellar/typescript-wallet-sdk-soroban" ], "scripts": { "prepare": "husky install", "lint": "eslint . --ext .ts", "test": "jest --watchAll", "test:ci": "jest --ci", + "test:e2e:ci": "yarn workspace @stellar/typescript-wallet-sdk test:e2e:ci", "test:recovery:ci": "yarn workspace @stellar/typescript-wallet-sdk test:recovery:ci", "test:anchorplatform:ci": "yarn workspace @stellar/typescript-wallet-sdk test:anchorplatform:ci", - "build": "yarn workspace @stellar/typescript-wallet-sdk build && yarn workspace @stellar/typescript-wallet-sdk-km build" + "build": "yarn workspace @stellar/typescript-wallet-sdk build && yarn workspace @stellar/typescript-wallet-sdk-km build && yarn workspace @stellar/typescript-wallet-sdk-soroban build" }, "lint-staged": { "**/*.ts": [ diff --git a/yarn.lock b/yarn.lock index 2445bad..3e6e65e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3411,6 +3411,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" @@ -4522,7 +4527,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: +fsevents@2.3.2, fsevents@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -6568,6 +6573,20 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.43.1: + version "1.43.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.43.1.tgz#0eafef9994c69c02a1a3825a4343e56c99c03b02" + integrity sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg== + +playwright@^1.43.1: + version "1.43.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.43.1.tgz#8ad08984ac66c9ef3d0db035be54dd7ec9f1c7d9" + integrity sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA== + dependencies: + playwright-core "1.43.1" + optionalDependencies: + fsevents "2.3.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"