From eef9596f1066f633f3bf1259d59453f834680f3a Mon Sep 17 00:00:00 2001 From: franklin251861-killer Date: Mon, 24 Aug 2026 19:40:58 +0000 Subject: [PATCH] feat: auto-generate TypeScript bindings from the contract ABI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/generate-bindings.sh which builds the payment_router contract and runs `contract bindings typescript` to produce a type-safe client in packages/types (published as @stellar-tags/payment-router). The generator is wired into the build via `npm run generate:bindings`, a CI check fails if the committed bindings drift from the ABI, and the dashboard's route_payment call now uses the generated client instead of hand-built SCVals and contract IDs. Closes #530 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .github/workflows/soroban.yml | 35 +++ README.md | 20 ++ package.json | 3 +- packages/types/.gitignore | 2 + packages/types/README.md | 54 ++++ packages/types/package.json | 20 ++ packages/types/src/index.ts | 316 ++++++++++++++++++++++ packages/types/tsconfig.json | 98 +++++++ payment-dashboard/package-lock.json | 16 ++ payment-dashboard/package.json | 1 + payment-dashboard/src/views/Dashboard.jsx | 49 +--- payment-dashboard/vite.config.js | 18 +- scripts/generate-bindings.sh | 120 ++++++++ 13 files changed, 716 insertions(+), 36 deletions(-) create mode 100644 packages/types/.gitignore create mode 100644 packages/types/README.md create mode 100644 packages/types/package.json create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/tsconfig.json create mode 100755 scripts/generate-bindings.sh diff --git a/.github/workflows/soroban.yml b/.github/workflows/soroban.yml index daf7561f..0516b27d 100644 --- a/.github/workflows/soroban.yml +++ b/.github/workflows/soroban.yml @@ -32,3 +32,38 @@ jobs: - name: Build contract to WASM run: cargo build --target wasm32-unknown-unknown --release + + bindings-check: + name: Bindings up-to-date + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Cache cargo registry and build artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: ./payment_router + + - name: Download stellar CLI (pinned) + run: | + curl -sL -o /tmp/stellar-cli.tar.gz \ + "https://github.com/stellar/stellar-cli/releases/download/v27.1.0/stellar-cli-27.1.0-x86_64-unknown-linux-gnu.tar.gz" + tar -xzf /tmp/stellar-cli.tar.gz -C /tmp + chmod +x /tmp/stellar + /tmp/stellar --version + + - name: Regenerate contract bindings + run: STELLAR_CLI=/tmp/stellar ./scripts/generate-bindings.sh + + - name: Fail if generated bindings drifted + run: | + if ! git diff --exit-code -- packages/; then + echo "::error::packages/types is out of date. Run 'npm run generate:bindings' and commit the result." + exit 1 + fi diff --git a/README.md b/README.md index 49a4a4a7..abeca966 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,26 @@ cd payment_router cargo build ``` +### Contract TypeScript bindings + +The TypeScript client for the `payment_router` contract lives in +[`packages/types`](packages/types) and is **auto-generated** from the contract +ABI, so the React dashboard gets end-to-end type safety with the Rust contract +instead of manually copying contract IDs and argument shapes. + +Regenerate the bindings after any change to the contract's public interface +(the contract must build with the `wasm32-unknown-unknown` target, and the +`stellar` CLI must be on your PATH): + +```bash +npm run generate:bindings +``` + +The result is committed to `packages/types` and consumed by the frontend as +`@stellar-tags/payment-router` (a `file:` dependency). The `bindings-check` CI +job fails the build if the checked-in bindings ever drift from the contract +ABI. + ## Tests ```bash diff --git a/package.json b/package.json index 3d41825f..10ffba0f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "start": "npm --prefix stellar-payment-platform start", "lint": "eslint .", "prepare": "husky", - "load:test": "artillery run artillery.yml" + "load:test": "artillery run artillery.yml", + "generate:bindings": "bash scripts/generate-bindings.sh" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/packages/types/.gitignore b/packages/types/.gitignore new file mode 100644 index 00000000..72aae85f --- /dev/null +++ b/packages/types/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +out/ diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 00000000..14e4e791 --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1,54 @@ +# @stellar-tags/payment-router JS + +JS library for interacting with [Soroban](https://soroban.stellar.org/) smart contract `@stellar-tags/payment-router` via Soroban RPC. + +This library was automatically generated by Soroban CLI using a command similar to: + +```bash +soroban contract bindings ts \ + --rpc-url INSERT_RPC_URL_HERE \ + --network-passphrase "INSERT_NETWORK_PASSPHRASE_HERE" \ + --contract-id INSERT_CONTRACT_ID_HERE \ + --output-dir ./path/to/payment-router +``` + +The network passphrase and contract ID are exported from [index.ts](./src/index.ts) in the `networks` constant. If you are the one who generated this library and you know that this contract is also deployed to other networks, feel free to update `networks` with other valid options. This will help your contract consumers use this library more easily. + +# To publish or not to publish + +This library is suitable for publishing to NPM. You can publish it to NPM using the `npm publish` command. + +But you don't need to publish this library to NPM to use it. You can add it to your project's `package.json` using a file path: + +```json +"dependencies": { + "@stellar-tags/payment-router": "./path/to/this/folder" +} +``` + +However, we've actually encountered [frustration](https://github.com/stellar/soroban-example-dapp/pull/117#discussion_r1232873560) using local libraries with NPM in this way. Though it seems a bit messy, we suggest generating the library directly to your `node_modules` folder automatically after each install by using a `postinstall` script. We've had the least trouble with this approach. NPM will automatically remove what it sees as erroneous directories during the `install` step, and then regenerate them when it gets to your `postinstall` step, which will keep the library up-to-date with your contract. + +```json +"scripts": { + "postinstall": "soroban contract bindings ts --rpc-url INSERT_RPC_URL_HERE --network-passphrase \"INSERT_NETWORK_PASSPHRASE_HERE\" --id INSERT_CONTRACT_ID_HERE --name @stellar-tags/payment-router" +} +``` + +Obviously you need to adjust the above command based on the actual command you used to generate the library. + +# Use it + +Now that you have your library up-to-date and added to your project, you can import it in a file and see inline documentation for all of its exported methods: + +```js +import { Contract, networks } from "@stellar-tags/payment-router" + +const contract = new Contract({ + ...networks.futurenet, // for example; check which networks this library exports + rpcUrl: '...', // use your own, or find one for testing at https://soroban.stellar.org/docs/reference/rpc#public-rpc-providers +}) + +contract.| +``` + +As long as your editor is configured to show JavaScript/TypeScript documentation, you can pause your typing at that `|` to get a list of all exports and inline-documentation for each. It exports a separate [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function for each method in the smart contract, with documentation for each generated from the comments the contract's author included in the original source code. diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 00000000..6a02e114 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,20 @@ +{ + "version": "0.0.0", + "name": "@stellar-tags/payment-router", + "type": "module", + "exports": "./src/index.ts", + "typings": "dist/index.d.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@stellar/stellar-sdk": "^16.0.1", + "buffer": "6.0.3" + }, + "devDependencies": { + "typescript": "^5.6.2" + }, + "description": "Auto-generated TypeScript client for the stellar-tags payment_router Soroban contract. Generated from the contract ABI by scripts/generate-bindings.sh — do not edit by hand.", + "main": "./src/index.ts", + "types": "./src/index.ts" +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 00000000..c00bf8c1 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,316 @@ +import { Buffer } from "buffer"; +import { Address } from "@stellar/stellar-sdk"; +import { + AssembledTransaction, + Client as ContractClient, + ClientOptions as ContractClientOptions, + MethodOptions, + Result, + Spec as ContractSpec, +} from "@stellar/stellar-sdk/contract"; +import type { + u32, + i32, + u64, + i64, + u128, + i128, + u256, + i256, + Option, + Timepoint, + Duration, +} from "@stellar/stellar-sdk/contract"; +export * from "@stellar/stellar-sdk"; +export * as contract from "@stellar/stellar-sdk/contract"; +export * as rpc from "@stellar/stellar-sdk/rpc"; + +if (typeof window !== "undefined") { + //@ts-ignore Buffer exists + window.Buffer = window.Buffer || Buffer; +} +/** + * Known deployments of the payment_router contract. The WASM-based generator + * cannot emit these (it has no network context), so scripts/generate-bindings.sh + * injects them after generation. + */ +export const networks = { + testnet: { + networkPassphrase: "Test SDF Network ; September 2015", + contractId: "CDNQ7OMHIFOLZHOKWQLOGDW7CF3DRMKXJC6OULNGNBWF4O4NO2NEIGER", + }, +} as const; + + + + + +/** + * Contract-level errors returned instead of panicking, so callers get a + * specific, stable error code to branch on rather than an opaque trap. + */ +export const Errors = { + /** + * Caller is not authorized to perform this action (e.g. not the admin). + */ + 1: {message:"Unauthorized"}, + /** + * Sender's token balance is lower than the requested payment amount. + */ + 2: {message:"InsufficientBalance"}, + /** + * Requested amount is outside allowed bounds, or a spending limit was exceeded. + */ + 3: {message:"LimitExceeded"}, + /** + * `initialize` was called on a contract that already has an admin set. + */ + 4: {message:"AlreadyInitialized"}, + /** + * An admin-configured value (treasury, fee, admin) was read before `initialize`. + */ + 5: {message:"NotInitialized"}, + 6: {message:"Paused"}, + 7: {message:"InvalidFeeRate"}, + /** + * Sender and recipient addresses are the same (self-routing not allowed). + */ + 8: {message:"InvalidRecipient"}, + /** + * Recipient address is blacklisted. + */ + 9: {message:"Blacklisted"} +} + +export type DataKey = {tag: "Admin", values: void} | {tag: "PlatformTreasury", values: void} | {tag: "FeeBps", values: void} | {tag: "FeeCap", values: void} | {tag: "Paused", values: void} | {tag: "MaxAmount", values: void} | {tag: "UserVolume", values: readonly [string]} | {tag: "UserSpending", values: readonly [string]} | {tag: "Blacklist", values: readonly [string]}; + + +export interface Payment { + amount: i128; + recipient: string; + sender: string; + token_address: string; +} + + +export interface UserSpending { + accumulated_amount: i128; + last_reset_time: u64; +} + +export interface Client { + /** + * Construct and simulate a get_fee transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Returns the current protocol fee percentage in basis points. + */ + get_fee: (options?: MethodOptions) => Promise> + + /** + * Construct and simulate a upgrade transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Replaces this contract's WASM with a previously uploaded version. Admin-only. + */ + upgrade: ({new_wasm_hash}: {new_wasm_hash: Buffer}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a version transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Returns the contract version. + */ + version: (options?: MethodOptions) => Promise> + + /** + * Construct and simulate a is_paused transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Returns whether the contract is currently paused. + */ + is_paused: (options?: MethodOptions) => Promise> + + /** + * Construct and simulate a set_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Set a new admin. Gated by the current admin if one exists. + */ + set_admin: ({new_admin}: {new_admin: string}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a set_pause transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Pauses or unpauses the payment router. Admin-only. + */ + set_pause: ({paused}: {paused: boolean}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a initialize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * One-time setup: records the admin and the initial fee configuration + * in instance storage. Must be called before `route_payment`. + */ + initialize: ({admin, platform_treasury, fee_bps, fee_cap, max_amount}: {admin: string, platform_treasury: string, fee_bps: i128, fee_cap: i128, max_amount: i128}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a set_paused transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Alias for `set_pause`. Admin-only. + */ + set_paused: ({paused}: {paused: boolean}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a set_fee_bps transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Updates the fee basis points. Admin-only. + */ + set_fee_bps: ({new_fee_bps}: {new_fee_bps: i128}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a route_payment transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Routes a payment from a sender to a recipient, deducting a platform fee. + */ + route_payment: ({sender, recipient, token_address, amount}: {sender: string, recipient: string, token_address: string, amount: i128}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a is_blacklisted transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Returns whether an address is blacklisted. + */ + is_blacklisted: ({address}: {address: string}, options?: MethodOptions) => Promise> + + /** + * Construct and simulate a recover_tokens transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Recovers tokens accidentally sent directly to the contract address. Admin-only. + */ + recover_tokens: ({token, amount}: {token: string, amount: i128}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a route_payments transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Routes multiple payments in a single transaction. If any payment fails, + * the entire batch is reverted atomically. + */ + route_payments: ({payments}: {payments: Array}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a set_fee_config transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Alias for `set_fee_config_legacy`. Admin-only. + */ + set_fee_config: ({fee_bps, fee_cap}: {fee_bps: i128, fee_cap: i128}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a transfer_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Transfers admin rights to a new address. Requires the current admin's authorization. + */ + transfer_admin: ({new_admin}: {new_admin: string}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a get_user_volume transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Returns the cumulative amount a given sender has routed through the contract. + */ + get_user_volume: ({user}: {user: string}, options?: MethodOptions) => Promise> + + /** + * Construct and simulate a blacklist_address transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Adds an address to the blacklist. Admin-only. + */ + blacklist_address: ({address}: {address: string}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a emergency_withdraw transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Admin-only emergency withdrawal of tokens held by this contract. + */ + emergency_withdraw: ({token, amount}: {token: string, amount: i128}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a add_supported_token transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Records a token as supported (no-op; routing accepts any token contract ID). + */ + add_supported_token: ({_token}: {_token: string}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a unblacklist_address transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Removes an address from the blacklist. Admin-only. + */ + unblacklist_address: ({address}: {address: string}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a get_effective_fee_bps transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Returns the effective fee_bps for a sender after applying any + * volume-based tiered discount. + */ + get_effective_fee_bps: ({sender}: {sender: string}, options?: MethodOptions) => Promise> + + /** + * Construct and simulate a set_fee_config_legacy transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Updates the fee basis points and fee cap. Admin-only. + */ + set_fee_config_legacy: ({fee_bps, fee_cap}: {fee_bps: i128, fee_cap: i128}, options?: MethodOptions) => Promise>> + + /** + * Construct and simulate a set_platform_treasury transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. + * Updates the treasury address that receives the platform fee. Admin-only. + */ + set_platform_treasury: ({new_treasury}: {new_treasury: string}, options?: MethodOptions) => Promise>> + +} +export class Client extends ContractClient { + static async deploy( + /** Options for initializing a Client as well as for calling a method, with extras specific to deploying. */ + options: MethodOptions & + Omit & { + /** The hash of the Wasm blob, which must already be installed on-chain. */ + wasmHash: Buffer | string; + /** Salt used to generate the contract's ID. Passed through to {@link Operation.createCustomContract}. Default: random. */ + salt?: Buffer | Uint8Array; + /** The format used to decode `wasmHash`, if it's provided as a string. */ + format?: "hex" | "base64"; + } + ): Promise> { + return ContractClient.deploy(null, options) + } + constructor(public readonly options: ContractClientOptions) { + super( + new ContractSpec([ "AAAAAAAAADxSZXR1cm5zIHRoZSBjdXJyZW50IHByb3RvY29sIGZlZSBwZXJjZW50YWdlIGluIGJhc2lzIHBvaW50cy4AAAAHZ2V0X2ZlZQAAAAAAAAAAAQAAAAs=", + "AAAAAAAAAE1SZXBsYWNlcyB0aGlzIGNvbnRyYWN0J3MgV0FTTSB3aXRoIGEgcHJldmlvdXNseSB1cGxvYWRlZCB2ZXJzaW9uLiBBZG1pbi1vbmx5LgAAAAAAAAd1cGdyYWRlAAAAAAEAAAAAAAAADW5ld193YXNtX2hhc2gAAAAAAAPuAAAAIAAAAAEAAAPpAAAD7QAAAAAAAAAD", + "AAAAAAAAAB1SZXR1cm5zIHRoZSBjb250cmFjdCB2ZXJzaW9uLgAAAAAAAAd2ZXJzaW9uAAAAAAAAAAABAAAABA==", + "AAAABAAAAIpDb250cmFjdC1sZXZlbCBlcnJvcnMgcmV0dXJuZWQgaW5zdGVhZCBvZiBwYW5pY2tpbmcsIHNvIGNhbGxlcnMgZ2V0IGEKc3BlY2lmaWMsIHN0YWJsZSBlcnJvciBjb2RlIHRvIGJyYW5jaCBvbiByYXRoZXIgdGhhbiBhbiBvcGFxdWUgdHJhcC4AAAAAAAAAAAAFRXJyb3IAAAAAAAAJAAAARUNhbGxlciBpcyBub3QgYXV0aG9yaXplZCB0byBwZXJmb3JtIHRoaXMgYWN0aW9uIChlLmcuIG5vdCB0aGUgYWRtaW4pLgAAAAAAAAxVbmF1dGhvcml6ZWQAAAABAAAAQlNlbmRlcidzIHRva2VuIGJhbGFuY2UgaXMgbG93ZXIgdGhhbiB0aGUgcmVxdWVzdGVkIHBheW1lbnQgYW1vdW50LgAAAAAAE0luc3VmZmljaWVudEJhbGFuY2UAAAAAAgAAAE1SZXF1ZXN0ZWQgYW1vdW50IGlzIG91dHNpZGUgYWxsb3dlZCBib3VuZHMsIG9yIGEgc3BlbmRpbmcgbGltaXQgd2FzIGV4Y2VlZGVkLgAAAAAAAA1MaW1pdEV4Y2VlZGVkAAAAAAAAAwAAAERgaW5pdGlhbGl6ZWAgd2FzIGNhbGxlZCBvbiBhIGNvbnRyYWN0IHRoYXQgYWxyZWFkeSBoYXMgYW4gYWRtaW4gc2V0LgAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAQAAABOQW4gYWRtaW4tY29uZmlndXJlZCB2YWx1ZSAodHJlYXN1cnksIGZlZSwgYWRtaW4pIHdhcyByZWFkIGJlZm9yZSBgaW5pdGlhbGl6ZWAuAAAAAAAOTm90SW5pdGlhbGl6ZWQAAAAAAAUAAAAAAAAABlBhdXNlZAAAAAAABgAAAAAAAAAOSW52YWxpZEZlZVJhdGUAAAAAAAcAAABHU2VuZGVyIGFuZCByZWNpcGllbnQgYWRkcmVzc2VzIGFyZSB0aGUgc2FtZSAoc2VsZi1yb3V0aW5nIG5vdCBhbGxvd2VkKS4AAAAAEEludmFsaWRSZWNpcGllbnQAAAAIAAAAIVJlY2lwaWVudCBhZGRyZXNzIGlzIGJsYWNrbGlzdGVkLgAAAAAAAAtCbGFja2xpc3RlZAAAAAAJ", + "AAAAAAAAADFSZXR1cm5zIHdoZXRoZXIgdGhlIGNvbnRyYWN0IGlzIGN1cnJlbnRseSBwYXVzZWQuAAAAAAAACWlzX3BhdXNlZAAAAAAAAAAAAAABAAAAAQ==", + "AAAAAAAAADpTZXQgYSBuZXcgYWRtaW4uIEdhdGVkIGJ5IHRoZSBjdXJyZW50IGFkbWluIGlmIG9uZSBleGlzdHMuAAAAAAAJc2V0X2FkbWluAAAAAAAAAQAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAEAAAPpAAAD7QAAAAAAAAAD", + "AAAAAAAAADJQYXVzZXMgb3IgdW5wYXVzZXMgdGhlIHBheW1lbnQgcm91dGVyLiBBZG1pbi1vbmx5LgAAAAAACXNldF9wYXVzZQAAAAAAAAEAAAAAAAAABnBhdXNlZAAAAAAAAQAAAAEAAAPpAAAD7QAAAAAAAAAD", + "AAAAAgAAAAAAAAAAAAAAB0RhdGFLZXkAAAAACQAAAAAAAAAAAAAABUFkbWluAAAAAAAAAAAAAAAAAAAQUGxhdGZvcm1UcmVhc3VyeQAAAAAAAAAAAAAABkZlZUJwcwAAAAAAAAAAAAAAAAAGRmVlQ2FwAAAAAAAAAAAAAAAAAAZQYXVzZWQAAAAAAAAAAAAAAAAACU1heEFtb3VudAAAAAAAAAEAAAAAAAAAClVzZXJWb2x1bWUAAAAAAAEAAAATAAAAAQAAAAAAAAAMVXNlclNwZW5kaW5nAAAAAQAAABMAAAABAAAAAAAAAAlCbGFja2xpc3QAAAAAAAABAAAAEw==", + "AAAAAQAAAAAAAAAAAAAAB1BheW1lbnQAAAAABAAAAAAAAAAGYW1vdW50AAAAAAALAAAAAAAAAAlyZWNpcGllbnQAAAAAAAATAAAAAAAAAAZzZW5kZXIAAAAAABMAAAAAAAAADXRva2VuX2FkZHJlc3MAAAAAAAAT", + "AAAAAAAAAH9PbmUtdGltZSBzZXR1cDogcmVjb3JkcyB0aGUgYWRtaW4gYW5kIHRoZSBpbml0aWFsIGZlZSBjb25maWd1cmF0aW9uCmluIGluc3RhbmNlIHN0b3JhZ2UuIE11c3QgYmUgY2FsbGVkIGJlZm9yZSBgcm91dGVfcGF5bWVudGAuAAAAAAppbml0aWFsaXplAAAAAAAFAAAAAAAAAAVhZG1pbgAAAAAAABMAAAAAAAAAEXBsYXRmb3JtX3RyZWFzdXJ5AAAAAAAAEwAAAAAAAAAHZmVlX2JwcwAAAAALAAAAAAAAAAdmZWVfY2FwAAAAAAsAAAAAAAAACm1heF9hbW91bnQAAAAAAAsAAAABAAAD6QAAA+0AAAAAAAAAAw==", + "AAAAAAAAACJBbGlhcyBmb3IgYHNldF9wYXVzZWAuIEFkbWluLW9ubHkuAAAAAAAKc2V0X3BhdXNlZAAAAAAAAQAAAAAAAAAGcGF1c2VkAAAAAAABAAAAAQAAA+kAAAPtAAAAAAAAAAM=", + "AAAAAAAAAClVcGRhdGVzIHRoZSBmZWUgYmFzaXMgcG9pbnRzLiBBZG1pbi1vbmx5LgAAAAAAAAtzZXRfZmVlX2JwcwAAAAABAAAAAAAAAAtuZXdfZmVlX2JwcwAAAAALAAAAAQAAA+kAAAPtAAAAAAAAAAM=", + "AAAAAAAAAEhSb3V0ZXMgYSBwYXltZW50IGZyb20gYSBzZW5kZXIgdG8gYSByZWNpcGllbnQsIGRlZHVjdGluZyBhIHBsYXRmb3JtIGZlZS4AAAANcm91dGVfcGF5bWVudAAAAAAAAAQAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAAAAAAJcmVjaXBpZW50AAAAAAAAEwAAAAAAAAANdG9rZW5fYWRkcmVzcwAAAAAAABMAAAAAAAAABmFtb3VudAAAAAAACwAAAAEAAAPpAAAD7QAAAAAAAAAD", + "AAAAAAAAACpSZXR1cm5zIHdoZXRoZXIgYW4gYWRkcmVzcyBpcyBibGFja2xpc3RlZC4AAAAAAA5pc19ibGFja2xpc3RlZAAAAAAAAQAAAAAAAAAHYWRkcmVzcwAAAAATAAAAAQAAAAE=", + "AAAAAAAAAE9SZWNvdmVycyB0b2tlbnMgYWNjaWRlbnRhbGx5IHNlbnQgZGlyZWN0bHkgdG8gdGhlIGNvbnRyYWN0IGFkZHJlc3MuIEFkbWluLW9ubHkuAAAAAA5yZWNvdmVyX3Rva2VucwAAAAAAAgAAAAAAAAAFdG9rZW4AAAAAAAATAAAAAAAAAAZhbW91bnQAAAAAAAsAAAABAAAD6QAAA+0AAAAAAAAAAw==", + "AAAAAAAAAHBSb3V0ZXMgbXVsdGlwbGUgcGF5bWVudHMgaW4gYSBzaW5nbGUgdHJhbnNhY3Rpb24uIElmIGFueSBwYXltZW50IGZhaWxzLAp0aGUgZW50aXJlIGJhdGNoIGlzIHJldmVydGVkIGF0b21pY2FsbHkuAAAADnJvdXRlX3BheW1lbnRzAAAAAAABAAAAAAAAAAhwYXltZW50cwAAA+oAAAfQAAAAB1BheW1lbnQAAAAAAQAAA+kAAAPtAAAAAAAAAAM=", + "AAAAAAAAAC5BbGlhcyBmb3IgYHNldF9mZWVfY29uZmlnX2xlZ2FjeWAuIEFkbWluLW9ubHkuAAAAAAAOc2V0X2ZlZV9jb25maWcAAAAAAAIAAAAAAAAAB2ZlZV9icHMAAAAACwAAAAAAAAAHZmVlX2NhcAAAAAALAAAAAQAAA+kAAAPtAAAAAAAAAAM=", + "AAAAAAAAAFRUcmFuc2ZlcnMgYWRtaW4gcmlnaHRzIHRvIGEgbmV3IGFkZHJlc3MuIFJlcXVpcmVzIHRoZSBjdXJyZW50IGFkbWluJ3MgYXV0aG9yaXphdGlvbi4AAAAOdHJhbnNmZXJfYWRtaW4AAAAAAAEAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAABAAAD6QAAA+0AAAAAAAAAAw==", + "AAAAAQAAAAAAAAAAAAAADFVzZXJTcGVuZGluZwAAAAIAAAAAAAAAEmFjY3VtdWxhdGVkX2Ftb3VudAAAAAAACwAAAAAAAAAPbGFzdF9yZXNldF90aW1lAAAAAAY=", + "AAAAAAAAAE1SZXR1cm5zIHRoZSBjdW11bGF0aXZlIGFtb3VudCBhIGdpdmVuIHNlbmRlciBoYXMgcm91dGVkIHRocm91Z2ggdGhlIGNvbnRyYWN0LgAAAAAAAA9nZXRfdXNlcl92b2x1bWUAAAAAAQAAAAAAAAAEdXNlcgAAABMAAAABAAAACw==", + "AAAAAAAAAC1BZGRzIGFuIGFkZHJlc3MgdG8gdGhlIGJsYWNrbGlzdC4gQWRtaW4tb25seS4AAAAAAAARYmxhY2tsaXN0X2FkZHJlc3MAAAAAAAABAAAAAAAAAAdhZGRyZXNzAAAAABMAAAABAAAD6QAAA+0AAAAAAAAAAw==", + "AAAAAAAAAEBBZG1pbi1vbmx5IGVtZXJnZW5jeSB3aXRoZHJhd2FsIG9mIHRva2VucyBoZWxkIGJ5IHRoaXMgY29udHJhY3QuAAAAEmVtZXJnZW5jeV93aXRoZHJhdwAAAAAAAgAAAAAAAAAFdG9rZW4AAAAAAAATAAAAAAAAAAZhbW91bnQAAAAAAAsAAAABAAAD6QAAA+0AAAAAAAAAAw==", + "AAAAAAAAAExSZWNvcmRzIGEgdG9rZW4gYXMgc3VwcG9ydGVkIChuby1vcDsgcm91dGluZyBhY2NlcHRzIGFueSB0b2tlbiBjb250cmFjdCBJRCkuAAAAE2FkZF9zdXBwb3J0ZWRfdG9rZW4AAAAAAQAAAAAAAAAGX3Rva2VuAAAAAAATAAAAAQAAA+kAAAPtAAAAAAAAAAM=", + "AAAAAAAAADJSZW1vdmVzIGFuIGFkZHJlc3MgZnJvbSB0aGUgYmxhY2tsaXN0LiBBZG1pbi1vbmx5LgAAAAAAE3VuYmxhY2tsaXN0X2FkZHJlc3MAAAAAAQAAAAAAAAAHYWRkcmVzcwAAAAATAAAAAQAAA+kAAAPtAAAAAAAAAAM=", + "AAAAAAAAAFtSZXR1cm5zIHRoZSBlZmZlY3RpdmUgZmVlX2JwcyBmb3IgYSBzZW5kZXIgYWZ0ZXIgYXBwbHlpbmcgYW55CnZvbHVtZS1iYXNlZCB0aWVyZWQgZGlzY291bnQuAAAAABVnZXRfZWZmZWN0aXZlX2ZlZV9icHMAAAAAAAABAAAAAAAAAAZzZW5kZXIAAAAAABMAAAABAAAACw==", + "AAAAAAAAADVVcGRhdGVzIHRoZSBmZWUgYmFzaXMgcG9pbnRzIGFuZCBmZWUgY2FwLiBBZG1pbi1vbmx5LgAAAAAAABVzZXRfZmVlX2NvbmZpZ19sZWdhY3kAAAAAAAACAAAAAAAAAAdmZWVfYnBzAAAAAAsAAAAAAAAAB2ZlZV9jYXAAAAAACwAAAAEAAAPpAAAD7QAAAAAAAAAD", + "AAAAAAAAAEhVcGRhdGVzIHRoZSB0cmVhc3VyeSBhZGRyZXNzIHRoYXQgcmVjZWl2ZXMgdGhlIHBsYXRmb3JtIGZlZS4gQWRtaW4tb25seS4AAAAVc2V0X3BsYXRmb3JtX3RyZWFzdXJ5AAAAAAAAAQAAAAAAAAAMbmV3X3RyZWFzdXJ5AAAAEwAAAAEAAAPpAAAD7QAAAAAAAAAD" ]), + options + ) + } + public readonly fromJSON = { + get_fee: this.txFromJSON, + upgrade: this.txFromJSON>, + version: this.txFromJSON, + is_paused: this.txFromJSON, + set_admin: this.txFromJSON>, + set_pause: this.txFromJSON>, + initialize: this.txFromJSON>, + set_paused: this.txFromJSON>, + set_fee_bps: this.txFromJSON>, + route_payment: this.txFromJSON>, + is_blacklisted: this.txFromJSON, + recover_tokens: this.txFromJSON>, + route_payments: this.txFromJSON>, + set_fee_config: this.txFromJSON>, + transfer_admin: this.txFromJSON>, + get_user_volume: this.txFromJSON, + blacklist_address: this.txFromJSON>, + emergency_withdraw: this.txFromJSON>, + add_supported_token: this.txFromJSON>, + unblacklist_address: this.txFromJSON>, + get_effective_fee_bps: this.txFromJSON, + set_fee_config_legacy: this.txFromJSON>, + set_platform_treasury: this.txFromJSON> + } +} \ No newline at end of file diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 00000000..acac1422 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,98 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "NodeNext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + // "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + // "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + // "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "src/*" + ] +} \ No newline at end of file diff --git a/payment-dashboard/package-lock.json b/payment-dashboard/package-lock.json index 621a11ad..c46dfee4 100644 --- a/payment-dashboard/package-lock.json +++ b/payment-dashboard/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.7.5", + "@stellar-tags/payment-router": "file:../packages/types", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^16.0.0", "date-fns": "^4.4.0", @@ -30,6 +31,17 @@ "vite-plugin-node-polyfills": "^0.28.0" } }, + "../packages/types": { + "name": "@stellar-tags/payment-router", + "version": "0.0.0", + "dependencies": { + "@stellar/stellar-sdk": "^16.0.1", + "buffer": "6.0.3" + }, + "devDependencies": { + "typescript": "^5.6.2" + } + }, "node_modules/@albedo-link/intent": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.12.0.tgz", @@ -2552,6 +2564,10 @@ "@stablelib/wipe": "^1.0.1" } }, + "node_modules/@stellar-tags/payment-router": { + "resolved": "../packages/types", + "link": true + }, "node_modules/@stellar/freighter-api": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-6.0.1.tgz", diff --git a/payment-dashboard/package.json b/payment-dashboard/package.json index 0ec3fb02..332c25d1 100644 --- a/payment-dashboard/package.json +++ b/payment-dashboard/package.json @@ -13,6 +13,7 @@ "@creit.tech/stellar-wallets-kit": "^1.7.5", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^16.0.0", + "@stellar-tags/payment-router": "file:../packages/types", "date-fns": "^4.4.0", "pino": "^9.0.0", "react": "^19.2.5", diff --git a/payment-dashboard/src/views/Dashboard.jsx b/payment-dashboard/src/views/Dashboard.jsx index a486f0f4..7ab39d31 100644 --- a/payment-dashboard/src/views/Dashboard.jsx +++ b/payment-dashboard/src/views/Dashboard.jsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import freighterApi from '@stellar/freighter-api'; import toast from 'react-hot-toast'; +import { Client as PaymentRouterClient, networks } from '@stellar-tags/payment-router'; import { useLatencyTracker } from '../useLatencyTracker'; import LatencyGauge from '../LatencyGauge'; import NetworkBadge from '../NetworkBadge'; @@ -11,13 +12,10 @@ import RecentAddresses from '../components/RecentAddresses'; import { useRecentAddresses } from '../useRecentAddresses'; import { API_BASE, - CONTRACT_ID, NAV_STORAGE_KEY, TOKEN_ADDRESS, - TREASURY_ADDRESS, formatShortAddress, formatUsername, - loadStellarSdk, resolveRecipient, apiErrorMessage, useNavState, @@ -239,40 +237,23 @@ function Dashboard({ } toast.loading("Simulating smart contract execution...", { id: toastId }); - const StellarSdk = await loadStellarSdk(); const amountStroops = BigInt(Math.floor(amountValue * 10000000)); - const contractArgs = [ - new StellarSdk.Address(userPublicKey).toScVal(), - new StellarSdk.Address(recipientAddress).toScVal(), - new StellarSdk.Address(TREASURY_ADDRESS).toScVal(), - new StellarSdk.Address(TOKEN_ADDRESS).toScVal(), - StellarSdk.nativeToScVal(amountStroops, { type: "i128" }), - ]; + // Build and simulate the payment through the type-safe client generated + // from the contract ABI (packages/types). + const client = new PaymentRouterClient({ + ...networks.testnet, + rpcUrl: "https://soroban-testnet.stellar.org", + }); - const server = new StellarSdk.rpc.Server( - "https://soroban-testnet.stellar.org", - ); - const account = await server.getAccount(userPublicKey); - const contract = new StellarSdk.Contract(CONTRACT_ID); - - const transaction = new StellarSdk.TransactionBuilder(account, { - fee: "100000", - networkPassphrase: "Test SDF Network ; September 2015", - }) - .addOperation(contract.call("route_payment", ...contractArgs)) - .setTimeout(300) - .build(); - - let preparedTransaction; + let assembledTransaction; try { - preparedTransaction = await server.prepareTransaction(transaction); - if (preparedTransaction.error) { - throw new Error( - preparedTransaction.error.message || - "Simulation rejected by network.", - ); - } + assembledTransaction = await client.route_payment({ + sender: userPublicKey, + recipient: recipientAddress, + token_address: TOKEN_ADDRESS, + amount: amountStroops, + }); } catch (err) { throw new Error(`Simulation failed: ${err.message}`, { cause: err }); } @@ -281,7 +262,7 @@ function Dashboard({ let signedXdrResponse; try { signedXdrResponse = await freighterApi.signTransaction( - preparedTransaction.toXDR(), + assembledTransaction.toXDR(), { network: "TESTNET", networkPassphrase: "Test SDF Network ; September 2015", diff --git a/payment-dashboard/vite.config.js b/payment-dashboard/vite.config.js index 8dd227e5..2ff41c05 100644 --- a/payment-dashboard/vite.config.js +++ b/payment-dashboard/vite.config.js @@ -6,8 +6,24 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills' export default defineConfig({ plugins: [ react(), - nodePolyfills(), + nodePolyfills({ + // The generated contract bindings (packages/types) import "buffer" + // directly; resolving it to the real npm package (hoisted by npm) + // avoids the plugin's alias shim, which does not resolve for files + // outside the dashboard root. + exclude: ['buffer'], + }), ], + resolve: { + dedupe: [ + // The bindings package lives outside the dashboard root (packages/types), + // so its own `@stellar/stellar-sdk` / `buffer` imports would otherwise + // resolve against the repo root. Dedupe forces every importer, including + // the linked bindings, to use the dashboard's installed copies. + '@stellar/stellar-sdk', + 'buffer', + ], + }, define: { global: 'globalThis', }, diff --git a/scripts/generate-bindings.sh b/scripts/generate-bindings.sh new file mode 100755 index 00000000..3d444d5d --- /dev/null +++ b/scripts/generate-bindings.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# +# Generates TypeScript bindings for the payment_router Soroban contract and +# writes them to packages/types. +# +# The bindings are derived from the contract ABI (spec) embedded in the WASM, +# so the contract is built first and `contract bindings typescript` is run +# against the resulting artifact. Generated files are checked into the repo so +# CI and the frontend never need the CLI installed — run this script whenever +# the contract's public interface changes and commit the result. +# +# Requirements: +# - cargo with the wasm32-unknown-unknown target installed (to build the contract) +# - the stellar (or soroban) CLI: https://github.com/stellar/stellar-cli +# +# You can point the script at a specific CLI binary with the STELLAR_CLI env +# var, e.g.: +# STELLAR_CLI=/opt/stellar-cli/bin/stellar ./scripts/generate-bindings.sh +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CONTRACT_DIR="$ROOT/payment_router" +PACKAGE_DIR="$ROOT/packages/types" +PACKAGE_NAME="@stellar-tags/payment-router" +WASM_REL="target/wasm32-unknown-unknown/release/payment_router.wasm" + +# --- 1. Locate the CLI ------------------------------------------------------ +CLI="${STELLAR_CLI:-}" +if [[ -z "$CLI" ]]; then + if command -v stellar >/dev/null 2>&1; then + CLI="stellar" + elif command -v soroban >/dev/null 2>&1; then + CLI="soroban" + else + echo "error: neither 'stellar' nor 'soroban' CLI found on PATH." >&2 + echo " Install it from https://github.com/stellar/stellar-cli or set STELLAR_CLI." >&2 + exit 1 + fi +fi + +echo "Using CLI: $CLI" + +# --- 2. Build the contract WASM --------------------------------------------- +echo "Building payment_router contract (wasm32-unknown-unknown, release)..." +cargo build --manifest-path "$CONTRACT_DIR/Cargo.toml" --target wasm32-unknown-unknown --release + +WASM="$CONTRACT_DIR/$WASM_REL" +if [[ ! -f "$WASM" ]]; then + echo "error: expected WASM artifact not found at $WASM" >&2 + exit 1 +fi + +# --- 3. Generate bindings into a temp dir ----------------------------------- +# The CLI names the package after the output directory, so generate into a +# temp dir with the desired name and move the result into place. +TMP_ROOT="$(mktemp -d)" +TMP_OUT="$TMP_ROOT/payment-router" +trap 'rm -rf "$TMP_ROOT"' EXIT + +echo "Generating TypeScript bindings..." +"$CLI" contract bindings typescript \ + --wasm "$WASM" \ + --output-dir "$TMP_OUT" \ + --overwrite + +# --- 4. Move into packages/types -------------------------------------------- +rm -rf "$PACKAGE_DIR" +mkdir -p "$(dirname "$PACKAGE_DIR")" +cp -R "$TMP_OUT" "$PACKAGE_DIR" +rm -rf "$TMP_ROOT" +trap - EXIT + +# --- 5. Normalize package metadata ------------------------------------------ +# The generated package.json points at a compiled dist/ that only exists after +# `tsc`. Point it at the checked-in TS source so Vite (and other bundlers) can +# consume the bindings directly with no build step. +node - "$PACKAGE_DIR/package.json" "$PACKAGE_NAME" <<'EOF' +const fs = require("fs"); +const [pkgPath, name] = process.argv.slice(2); +const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); +pkg.name = name; +pkg.description = + "Auto-generated TypeScript client for the stellar-tags payment_router " + + "Soroban contract. Generated from the contract ABI by " + + "scripts/generate-bindings.sh \u2014 do not edit by hand."; +pkg.exports = "./src/index.ts"; +pkg.main = "./src/index.ts"; +pkg.types = "./src/index.ts"; +fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); +EOF + +# --- 6. Inject network metadata ---------------------------------------------- +# Generated-from-WASM bindings have no network context, so the CLI cannot emit +# the `networks` constant. Inject it (idempotently) so consumers get the +# contract ID and passphrase from the shared package. +node - "$PACKAGE_DIR/src/index.ts" "$PACKAGE_NAME" <<'EOF' +const fs = require("fs"); +const [filePath, name] = process.argv.slice(2); +let src = fs.readFileSync(filePath, "utf8"); +if (!src.includes("export const networks")) { + const networks = `\n/**\n * Known deployments of the payment_router contract. The WASM-based generator\n * cannot emit these (it has no network context), so scripts/generate-bindings.sh\n * injects them after generation.\n */\nexport const networks = {\n testnet: {\n networkPassphrase: "Test SDF Network ; September 2015",\n contractId: "CDNQ7OMHIFOLZHOKWQLOGDW7CF3DRMKXJC6OULNGNBWF4O4NO2NEIGER",\n },\n} as const;\n`; + const marker = 'window.Buffer = window.Buffer || Buffer;\n}'; + const idx = src.indexOf(marker); + if (idx !== -1) { + src = src.slice(0, idx + marker.length) + networks + src.slice(idx + marker.length); + } else { + src += networks; + } + fs.writeFileSync(filePath, src); +} +EOF + +# The generated README references the temp-dir package name; swap it for the +# real scoped name so the checked-in docs stay accurate (keeping the +# illustrative output-dir path un-scoped). +sed -i "s|\bpayment-router\b|$PACKAGE_NAME|g" "$PACKAGE_DIR/README.md" +sed -i "s|\./path/to/$PACKAGE_NAME|./path/to/payment-router|g" "$PACKAGE_DIR/README.md" + +echo "Done. Bindings written to $PACKAGE_DIR"