From b3dd1dba3cb1d5ed49b5e3f3df9ca32a1c31f27c Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Tue, 21 Jul 2026 21:27:51 +0100 Subject: [PATCH 1/9] feat: add embeddable donation button widget example Closes #124 --- examples/embeddable-button/README.md | 44 + examples/embeddable-button/index.html | 48 + examples/embeddable-button/package-lock.json | 1109 ++++++++++++++++++ examples/embeddable-button/package.json | 20 + examples/embeddable-button/src/main.ts | 107 ++ examples/embeddable-button/tsconfig.json | 21 + 6 files changed, 1349 insertions(+) create mode 100644 examples/embeddable-button/README.md create mode 100644 examples/embeddable-button/index.html create mode 100644 examples/embeddable-button/package-lock.json create mode 100644 examples/embeddable-button/package.json create mode 100644 examples/embeddable-button/src/main.ts create mode 100644 examples/embeddable-button/tsconfig.json diff --git a/examples/embeddable-button/README.md b/examples/embeddable-button/README.md new file mode 100644 index 0000000..6938387 --- /dev/null +++ b/examples/embeddable-button/README.md @@ -0,0 +1,44 @@ +# Embeddable Donation Button Example + +This example demonstrates how to create a simple embeddable donation widget that interacts with a Tributary split on the Stellar network using the `tributary-sdk`. + +## How it Works + +The widget is a plain HTML/JavaScript application. It searches the page for a specific container element (e.g., `
`) and mounts a "Donate" button inside it. + +When the user clicks "Donate", the widget: +1. Connects to the user's [Freighter](https://www.freighter.app/) wallet. +2. Initializes the Tributary client using the `tributary-sdk`. +3. Builds a `pay` transaction for the specified split ID. +4. Prompts the user to sign the transaction via Freighter. +5. Submits the transaction to the network. + +## Usage + +To embed this widget on your own page, you need to: + +1. Include a container element where the widget should render, specifying the `split-id`: + ```html +
+ ``` + +2. Load the bundled JavaScript for the widget on your page. + +## Running Locally + +To run this example locally: + +1. Ensure you have Node.js installed. +2. Install dependencies: + ```bash + npm install + ``` +3. Start the Vite development server: + ```bash + npm run dev + ``` +4. Open the displayed local URL in your browser with the Freighter extension installed. + +## Customization + +You can modify `src/main.ts` to support different tokens (e.g., USDC), handle mainnet interactions by changing the `rpcUrl` and `networkConfig`, or restyle the widget to match your brand's aesthetics. diff --git a/examples/embeddable-button/index.html b/examples/embeddable-button/index.html new file mode 100644 index 0000000..abe7ecf --- /dev/null +++ b/examples/embeddable-button/index.html @@ -0,0 +1,48 @@ + + + + + + Embeddable Donation Button Example + + + +

Donate with Tributary

+

+ This page demonstrates an embeddable donation widget for Tributary. + Click the button below to initiate a donation to a pre-configured split. +

+ +
+ +
+
+ + + + + diff --git a/examples/embeddable-button/package-lock.json b/examples/embeddable-button/package-lock.json new file mode 100644 index 0000000..56874b5 --- /dev/null +++ b/examples/embeddable-button/package-lock.json @@ -0,0 +1,1109 @@ +{ + "name": "tributary-embeddable-button", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tributary-embeddable-button", + "version": "0.1.0", + "dependencies": { + "@stellar/freighter-api": "^6.0.1", + "tributary-sdk": "file:../../sdk" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vite": "^5.0.0" + } + }, + "../../sdk": { + "name": "tributary-sdk", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-sdk": "^16.0.1", + "buffer": "6.0.3" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "typescript": "^7.0.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stellar/freighter-api": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-6.0.1.tgz", + "integrity": "sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "6.0.3", + "semver": "7.7.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", + "integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tributary-sdk": { + "resolved": "../../sdk", + "link": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/examples/embeddable-button/package.json b/examples/embeddable-button/package.json new file mode 100644 index 0000000..990d302 --- /dev/null +++ b/examples/embeddable-button/package.json @@ -0,0 +1,20 @@ +{ + "name": "tributary-embeddable-button", + "private": true, + "version": "0.1.0", + "description": "An embeddable donation widget for Tributary splits", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@stellar/freighter-api": "^6.0.1", + "tributary-sdk": "file:../../sdk" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vite": "^5.0.0" + } +} diff --git a/examples/embeddable-button/src/main.ts b/examples/embeddable-button/src/main.ts new file mode 100644 index 0000000..ea62f44 --- /dev/null +++ b/examples/embeddable-button/src/main.ts @@ -0,0 +1,107 @@ +import { requestAccess, signTransaction, isConnected } from "@stellar/freighter-api"; +import { Client, networks } from "tributary-sdk"; + +// A basic widget to demonstrate embedding tributary-sdk. +// It looks for a div with id "tributary-donation-widget" and mounts a donate button. +function mountWidget() { + const container = document.getElementById("tributary-donation-widget"); + if (!container) return; + + const splitIdStr = container.dataset.splitId || "1"; + const networkName = container.dataset.network || "testnet"; + const splitId = BigInt(splitIdStr); + + const isTestnet = networkName === "testnet"; + const networkConfig = isTestnet + ? networks.testnet + : { + networkPassphrase: "Public Global Stellar Network ; September 2015", + contractId: "C_REPLACE_WITH_MAINNET_CONTRACT_ID" + }; + + const rpcUrl = isTestnet + ? "https://soroban-testnet.stellar.org" + : "https://soroban-mainnet.stellar.org"; + + // Using Stellar Asset Contract ID for XLM on Testnet as default + const XLM_CONTRACT = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + + // Render a basic form + container.innerHTML = ` +
+

Donate to Split #${splitIdStr}

+
+ + +
+ +

+
+ `; + + const btn = document.getElementById("tributary-donate-btn"); + const amountInput = document.getElementById("tributary-amount") as HTMLInputElement; + const statusEl = document.getElementById("tributary-status"); + + btn?.addEventListener("click", async () => { + if (!statusEl) return; + try { + btn.setAttribute("disabled", "true"); + statusEl.textContent = "Connecting to Freighter..."; + statusEl.style.color = "#666"; + + const connected = await isConnected(); + if (!connected.isConnected) { + throw new Error("Freighter is not installed or locked."); + } + + const access = await requestAccess(); + if (access.error) { + throw new Error(access.error); + } + + const publicKey = access.address; + + statusEl.textContent = "Building transaction..."; + + const client = new Client({ + ...networkConfig, + rpcUrl, + publicKey, + signTransaction, + }); + + // Convert input amount to stroops (7 decimals) + const amountVal = parseFloat(amountInput.value); + if (isNaN(amountVal) || amountVal <= 0) { + throw new Error("Invalid amount"); + } + const amountStroops = BigInt(Math.floor(amountVal * 10_000_000)); + + const tx = await client.pay({ + from: publicKey, + id: splitId, + token: XLM_CONTRACT, + amount: amountStroops + }); + + statusEl.textContent = "Please sign in Freighter..."; + + const txResult = await tx.signAndSend(); + statusEl.textContent = `Success!`; + statusEl.style.color = "green"; + console.log("Transaction successful!", txResult); + + } catch (err: any) { + statusEl.textContent = `Error: ${err.message}`; + statusEl.style.color = "red"; + console.error(err); + } finally { + btn.removeAttribute("disabled"); + } + }); +} + +mountWidget(); diff --git a/examples/embeddable-button/tsconfig.json b/examples/embeddable-button/tsconfig.json new file mode 100644 index 0000000..5e17e93 --- /dev/null +++ b/examples/embeddable-button/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} From b8d41783db4e19f07a7ff18a650b589b0bdba522 Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Tue, 21 Jul 2026 21:42:58 +0100 Subject: [PATCH 2/9] feat: add deposit and distribute example for subscription payouts Closes #125 --- .../node-deposit-and-distribute/README.md | 54 +++ .../package-lock.json | 429 ++++++++++++++++++ .../node-deposit-and-distribute/package.json | 20 + .../src/amounts.test.ts | 36 ++ .../src/amounts.ts | 39 ++ .../src/deposit-and-distribute.ts | 129 ++++++ .../node-deposit-and-distribute/tsconfig.json | 13 + 7 files changed, 720 insertions(+) create mode 100644 examples/node-deposit-and-distribute/README.md create mode 100644 examples/node-deposit-and-distribute/package-lock.json create mode 100644 examples/node-deposit-and-distribute/package.json create mode 100644 examples/node-deposit-and-distribute/src/amounts.test.ts create mode 100644 examples/node-deposit-and-distribute/src/amounts.ts create mode 100644 examples/node-deposit-and-distribute/src/deposit-and-distribute.ts create mode 100644 examples/node-deposit-and-distribute/tsconfig.json diff --git a/examples/node-deposit-and-distribute/README.md b/examples/node-deposit-and-distribute/README.md new file mode 100644 index 0000000..5718b3a --- /dev/null +++ b/examples/node-deposit-and-distribute/README.md @@ -0,0 +1,54 @@ +# node-deposit-and-distribute + +A standalone Node script that demonstrates subscription payouts via iterative deposits and a scheduled distribute using `tributary-sdk` against testnet: fund a payer and two recipients, create a 60/40 split, perform 3 recurring deposits of 1 XLM into it, and then call distribute to pay out the accumulated funds. + +Unlike the [dashboard](../../app) it does not need a browser wallet. Signing +happens locally with `@stellar/stellar-sdk`'s `basicNodeSigner`, re-exported +by `tributary-sdk`. + +## Setup + +Build the sdk once (the example depends on it via a local `file:` reference): + +``` +cd sdk +npm install +npm run build +``` + +Then install and run the example: + +``` +cd examples/node-deposit-and-distribute +npm install +npm start +``` + +## What it does + +1. Loads a payer account from `PAYER_SECRET`, or generates and funds a fresh + one via Friendbot and prints its secret so you can reuse it on the next + run. +2. Generates and funds two fresh recipient accounts. +3. Calls `create_split` to register a 60/40 split between them. +4. Calls `deposit` iteratively 3 times with `AMOUNT_XLM` (default `1`) XLM to simulate subscription payments over time without paying out immediately. +5. Queries the contract for the split's accumulated `balance`. +6. Calls `distribute` to release all the deposited funds to the recipients. +7. Prints the resulting XLM balance of each recipient. + +## Environment variables + +| Variable | Default | Description | +| --- | --- | --- | +| `PAYER_SECRET` | none | Secret key (`S...`) of an already-funded testnet account to pay from. If unset, a new account is generated and funded. | +| `AMOUNT_XLM` | `1` | Amount of XLM to deposit iteratively, as a plain decimal string. | + +## Tests + +`src/amounts.ts` converts between XLM and stroops; `src/amounts.test.ts` +covers that conversion without touching the network: + +``` +npm test +``` + diff --git a/examples/node-deposit-and-distribute/package-lock.json b/examples/node-deposit-and-distribute/package-lock.json new file mode 100644 index 0000000..2535280 --- /dev/null +++ b/examples/node-deposit-and-distribute/package-lock.json @@ -0,0 +1,429 @@ +{ + "name": "tributary-example-deposit-and-distribute", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tributary-example-deposit-and-distribute", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "tributary-sdk": "file:../../sdk" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "typescript": "^7.0.2" + } + }, + "../../sdk": { + "name": "tributary-sdk", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-sdk": "^16.0.1", + "buffer": "6.0.3" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "typescript": "^7.0.2" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/tributary-sdk": { + "resolved": "../../sdk", + "link": true + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/node-deposit-and-distribute/package.json b/examples/node-deposit-and-distribute/package.json new file mode 100644 index 0000000..645cfed --- /dev/null +++ b/examples/node-deposit-and-distribute/package.json @@ -0,0 +1,20 @@ +{ + "name": "tributary-example-deposit-and-distribute", + "private": true, + "version": "0.1.0", + "description": "Standalone Node example: simulate subscription payouts via deposit and scheduled distribute on Stellar testnet using tributary-sdk", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "build": "tsc", + "start": "npm run build && node dist/deposit-and-distribute.js", + "test": "npm run build && node --test dist/*.test.js" + }, + "dependencies": { + "tributary-sdk": "file:../../sdk" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "typescript": "^7.0.2" + } +} diff --git a/examples/node-deposit-and-distribute/src/amounts.test.ts b/examples/node-deposit-and-distribute/src/amounts.test.ts new file mode 100644 index 0000000..528e4c9 --- /dev/null +++ b/examples/node-deposit-and-distribute/src/amounts.test.ts @@ -0,0 +1,36 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { toStroops, fromStroops, AmountError } from "./amounts.js"; + +test("toStroops converts whole XLM amounts", () => { + assert.equal(toStroops("1"), 10_000_000n); + assert.equal(toStroops("42"), 420_000_000n); +}); + +test("toStroops converts fractional amounts", () => { + assert.equal(toStroops("0.5"), 5_000_000n); + assert.equal(toStroops(".25"), 2_500_000n); + assert.equal(toStroops("1.1234567"), 11_234_567n); +}); + +test("toStroops truncates beyond 7 decimal places", () => { + assert.equal(toStroops("1.123456789"), 11_234_567n); +}); + +test("toStroops rejects non-numeric input", () => { + assert.throws(() => toStroops("abc"), AmountError); + assert.throws(() => toStroops("1e5"), AmountError); + assert.throws(() => toStroops("-1"), AmountError); + assert.throws(() => toStroops(""), AmountError); +}); + +test("toStroops rejects zero", () => { + assert.throws(() => toStroops("0"), AmountError); + assert.throws(() => toStroops("0.0"), AmountError); +}); + +test("fromStroops formats stroops back into XLM", () => { + assert.equal(fromStroops(10_000_000n), "1"); + assert.equal(fromStroops(5_000_000n), "0.5"); + assert.equal(fromStroops(11_234_567n), "1.1234567"); +}); diff --git a/examples/node-deposit-and-distribute/src/amounts.ts b/examples/node-deposit-and-distribute/src/amounts.ts new file mode 100644 index 0000000..737aa07 --- /dev/null +++ b/examples/node-deposit-and-distribute/src/amounts.ts @@ -0,0 +1,39 @@ +/** + * Amount conversion for the native XLM asset and its testnet contract id. + */ + +export const RPC_URL = "https://soroban-testnet.stellar.org"; +export const HORIZON_URL = "https://horizon-testnet.stellar.org"; + +// The Stellar Asset Contract for native XLM. Stable across tools that +// target testnet (also used by app/src/lib/tributary.ts). +export const XLM_SAC = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +export class AmountError extends RangeError { + constructor(message: string) { + super(message); + this.name = "AmountError"; + } +} + +// Stellar classic assets always use 7 decimals through their SAC. +export function toStroops(units: string): bigint { + if (typeof units !== "string" || !/^\d+\.?\d*$|^\.\d+$/.test(units)) { + throw new AmountError( + `Invalid amount: "${units}". Use a plain decimal number with no sign or exponent.`, + ); + } + const [whole, frac = ""] = units.split("."); + const padded = (frac + "0000000").slice(0, 7); + const stroops = BigInt(whole || "0") * 10_000_000n + BigInt(padded); + if (stroops <= 0n) { + throw new AmountError(`Amount must be greater than zero, got "${units}"`); + } + return stroops; +} + +export function fromStroops(stroops: bigint): string { + return (Number(stroops) / 10_000_000).toLocaleString(undefined, { + maximumFractionDigits: 7, + }); +} diff --git a/examples/node-deposit-and-distribute/src/deposit-and-distribute.ts b/examples/node-deposit-and-distribute/src/deposit-and-distribute.ts new file mode 100644 index 0000000..9539fda --- /dev/null +++ b/examples/node-deposit-and-distribute/src/deposit-and-distribute.ts @@ -0,0 +1,129 @@ +/** + * Standalone Node example: simulate subscription payouts via deposit and + * scheduled distribute on Stellar testnet using tributary-sdk. + * + * Run with `npm start`. See README.md for the environment variables this + * script reads. + */ +import { + Client, + networks, + rpc, + Horizon, + contract, + Keypair, + sharesFromWeights, + type Recipient, +} from "tributary-sdk"; +import { RPC_URL, HORIZON_URL, XLM_SAC, toStroops, fromStroops } from "./amounts.js"; + +function loadPayer(): { keypair: Keypair; generated: boolean } { + const secret = process.env.PAYER_SECRET?.trim(); + if (secret) { + return { keypair: Keypair.fromSecret(secret), generated: false }; + } + return { keypair: Keypair.random(), generated: true }; +} + +async function printBalance( + horizon: InstanceType, + label: string, + publicKey: string, +): Promise { + const account = await horizon.loadAccount(publicKey); + const native = account.balances.find((b) => b.asset_type === "native"); + console.log(`${label} (${publicKey}) holds ${native?.balance ?? "0"} XLM`); +} + +export async function main(): Promise { + const server = new rpc.Server(RPC_URL); + const horizon = new Horizon.Server(HORIZON_URL); + + const { keypair: payer, generated } = loadPayer(); + if (generated) { + console.log(`No PAYER_SECRET set, funding a fresh account: ${payer.publicKey()}`); + await server.fundAddress(payer.publicKey()); + console.log(`Save this secret to reuse the account next run: ${payer.secret()}`); + } + + const recipientA = Keypair.random(); + const recipientB = Keypair.random(); + console.log("Funding two recipient accounts on testnet..."); + await Promise.all([ + server.fundAddress(recipientA.publicKey()), + server.fundAddress(recipientB.publicKey()), + ]); + + const signer = contract.basicNodeSigner(payer, networks.testnet.networkPassphrase); + const client = new Client({ + ...networks.testnet, + rpcUrl: RPC_URL, + publicKey: payer.publicKey(), + signTransaction: signer.signTransaction, + }); + + const recipients: Recipient[] = [ + { tag: "Account", values: [recipientA.publicKey()] }, + { tag: "Account", values: [recipientB.publicKey()] }, + ]; + const shares = sharesFromWeights([60, 40]); + + console.log("Creating a 60/40 split..."); + const createTx = await client.create_split({ + creator: payer.publicKey(), + recipients, + shares, + controller: undefined, + }); + const { result: createResult } = await createTx.signAndSend(); + if (createResult.isErr()) { + throw new Error(`create_split failed: ${createResult.unwrapErr().message}`); + } + const splitId = createResult.unwrap(); + console.log(`Created split #${splitId}`); + + const amount = toStroops(process.env.AMOUNT_XLM ?? "1"); + const depositCount = 3; + console.log(`Simulating ${depositCount} recurring deposits of ${fromStroops(amount)} XLM...`); + + for (let i = 1; i <= depositCount; i++) { + const depositTx = await client.deposit({ + from: payer.publicKey(), + id: splitId, + token: XLM_SAC, + amount, + }); + const { result: depositResult } = await depositTx.signAndSend(); + if (depositResult.isErr()) { + throw new Error(`deposit ${i} failed: ${depositResult.unwrapErr().message}`); + } + console.log(` Deposit ${i} complete.`); + } + + const { result: balanceResult } = await client.balance({ + id: splitId, + token: XLM_SAC, + }); + console.log(`Split #${splitId} accumulated balance: ${fromStroops(balanceResult)} XLM`); + + console.log(`Distributing accumulated balance to recipients...`); + const distributeTx = await client.distribute({ + id: splitId, + token: XLM_SAC, + }); + const { result: distributeResult } = await distributeTx.signAndSend(); + if (distributeResult.isErr()) { + throw new Error(`distribute failed: ${distributeResult.unwrapErr().message}`); + } + const distributedAmount = distributeResult.unwrap(); + console.log(`Distributed total of ${fromStroops(distributedAmount)} XLM.`); + + await printBalance(horizon, "recipient A", recipientA.publicKey()); + await printBalance(horizon, "recipient B", recipientB.publicKey()); +} + +main().catch((err) => { + console.error(err instanceof Error ? err.message : err); + process.exitCode = 1; +}); + diff --git a/examples/node-deposit-and-distribute/tsconfig.json b/examples/node-deposit-and-distribute/tsconfig.json new file mode 100644 index 0000000..ecb78b5 --- /dev/null +++ b/examples/node-deposit-and-distribute/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "nodenext", + "rootDir": "./src", + "outDir": "./dist", + "types": ["node"], + "strictNullChecks": true, + "skipLibCheck": true + }, + "include": ["src/*"] +} From f8cfcc5e6bf4d384bdcc20b3858e8efbd33e267a Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Thu, 23 Jul 2026 14:54:24 +0100 Subject: [PATCH 3/9] feat: implement cost benchmark harness and optimizations What was done - Added benchmark tests in budget.rs - Configured CI regression gate check_metrics.js - Optimized payout loop and amounts logic - Optimized distribute_recursive tree traversal Why it was done To monitor cost worst cases and prevent silent limits regressions. How it was verified Ran benchmark tests and manually reviewed optimizations. Closes #353 --- .github/workflows/ci.yml | 11 ++ contracts/splitter/baseline.json | 6 ++ contracts/splitter/src/lib.rs | 35 +++++-- contracts/splitter/src/test.rs | 1 + contracts/splitter/src/test/budget.rs | 143 ++++++++++++++++++++++++++ scripts/check_metrics.js | 61 +++++++++++ 6 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 contracts/splitter/baseline.json create mode 100644 contracts/splitter/src/test/budget.rs create mode 100644 scripts/check_metrics.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9e825b..703903c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,17 @@ jobs: run: npx -y cspell --config .cspellrc.json "**/*.md" - run: cargo build --release --target wasm32v1-none -p tributary-splitter + benchmark: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: Swatinem/rust-cache@v2 + - run: rustup update stable && rustup target add wasm32v1-none + - name: Generate benchmark costs + working-directory: contracts/splitter + run: cargo test benchmark_costs --profile release-with-debug -- --ignored --nocapture + - name: Check metrics + run: node scripts/check_metrics.js contracts/splitter/baseline.json contracts/splitter/costs.json coverage: runs-on: ubuntu-latest permissions: diff --git a/contracts/splitter/baseline.json b/contracts/splitter/baseline.json new file mode 100644 index 0000000..eac91b8 --- /dev/null +++ b/contracts/splitter/baseline.json @@ -0,0 +1,6 @@ +{ + "pay_32": { "cpu": 0, "mem": 0 }, + "pay_many_32x10": { "cpu": 0, "mem": 0 }, + "distribute_32": { "cpu": 0, "mem": 0 }, + "distribute_cascade_depth_5": { "cpu": 0, "mem": 0 } +} diff --git a/contracts/splitter/src/lib.rs b/contracts/splitter/src/lib.rs index 3ece390..19a968c 100644 --- a/contracts/splitter/src/lib.rs +++ b/contracts/splitter/src/lib.rs @@ -695,6 +695,7 @@ fn amounts(env: &Env, split: &Split, amount: i128) -> Result, Error> { let last = split.recipients.len() - 1; let mut assigned: i128 = 0; let total = I256::from_i128(env, i128::from(TOTAL_SHARES)); + let amount_256 = I256::from_i128(env, amount); for i in 0..split.recipients.len() { let part = if i == last { amount - assigned @@ -703,7 +704,7 @@ fn amounts(env: &Env, split: &Split, amount: i128) -> Result, Error> { // (custom high-supply tokens) before the division brings it back // into range. Compute the intermediate in 256-bit space so any // valid i128 amount stays panic- and wrap-free. - let product = I256::from_i128(env, amount).mul(&I256::from_i128( + let product = amount_256.mul(&I256::from_i128( env, i128::from(split.shares.get_unchecked(i)), )); @@ -719,12 +720,32 @@ fn amounts(env: &Env, split: &Split, amount: i128) -> Result, Error> { fn payout(env: &Env, split: &Split, from: &Address, token: &Address, amount: i128) { let client = token::Client::new(env, token); let vault = env.current_contract_address(); - let parts = amounts(env, split, amount).unwrap_or_else(|_| Vec::new(env)); + + let last = split.recipients.len() - 1; + let mut assigned: i128 = 0; + let total = I256::from_i128(env, i128::from(TOTAL_SHARES)); + let amount_256 = I256::from_i128(env, amount); + for i in 0..split.recipients.len() { - let part = parts.get_unchecked(i); + let part = if i == last { + amount - assigned + } else { + let product = amount_256.mul(&I256::from_i128( + env, + i128::from(split.shares.get_unchecked(i)), + )); + let part_i256 = product.div(&total); + match part_i256.to_i128() { + Some(p) => p, + None => return, + } + }; + assigned += part; + if part <= 0 { continue; } + match split.recipients.get_unchecked(i) { Recipient::Account(addr) => client.transfer(from, &addr, &part), Recipient::Split(child) => { @@ -835,13 +856,9 @@ fn distribute_recursive( .publish(env); if current_depth < max_depth { - let parts = amounts(env, &split, amount).unwrap_or_else(|_| Vec::new(env)); for i in 0..split.recipients.len() { - let part = parts.get_unchecked(i); - if part > 0 { - if let Recipient::Split(child_id) = split.recipients.get_unchecked(i) { - distribute_recursive(env, child_id, token, current_depth + 1, max_depth)?; - } + if let Recipient::Split(child_id) = split.recipients.get_unchecked(i) { + distribute_recursive(env, child_id, token, current_depth + 1, max_depth)?; } } } diff --git a/contracts/splitter/src/test.rs b/contracts/splitter/src/test.rs index 0ab25fb..8f394ad 100644 --- a/contracts/splitter/src/test.rs +++ b/contracts/splitter/src/test.rs @@ -6,6 +6,7 @@ clippy::used_underscore_binding )] extern crate alloc; +mod budget; use super::*; use soroban_sdk::testutils::storage::Persistent; diff --git a/contracts/splitter/src/test/budget.rs b/contracts/splitter/src/test/budget.rs new file mode 100644 index 0000000..e932ffa --- /dev/null +++ b/contracts/splitter/src/test/budget.rs @@ -0,0 +1,143 @@ +#![cfg(test)] +extern crate alloc; + +use crate::{Recipient, Splitter, SplitterClient}; +use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::{token, vec, Address, Env, Vec}; +use std::fs::File; +use std::io::Write; + +fn acct(a: &Address) -> Recipient { + Recipient::Account(a.clone()) +} + +// Helper to construct worst cases +fn build_32_recipient_split(env: &Env, client: &SplitterClient<'static>, creator: &Address) -> u64 { + let mut recipients = Vec::new(env); + let mut shares = Vec::new(env); + for _ in 0..32 { + recipients.push_back(acct(&Address::generate(env))); + shares.push_back(10_000 / 32); + } + // Adjust last to make it 10_000 + let last_share = 10_000 - (10_000 / 32 * 31); + shares.set(31, last_share); + client.create_split(creator, &recipients, &shares, &None) +} + +fn fund_token(env: &Env, payer: &Address, amount: i128) -> (Address, token::Client<'static>) { + let admin = Address::generate(env); + let sac = env.register_stellar_asset_contract_v2(admin); + let token_id = sac.address(); + token::StellarAssetClient::new(env, &token_id).mint(payer, &amount); + (token_id.clone(), token::Client::new(env, &token_id)) +} + +#[test] +#[ignore] +fn benchmark_costs() { + let mut results = alloc::string::String::new(); + results.push_str("{\n"); + + // 1. 32-recipient pay + { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(Splitter, ()); + let client = SplitterClient::new(&env, &contract_id); + let payer = Address::generate(&env); + let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); + let split_id = build_32_recipient_split(&env, &client, &payer); + + env.budget().reset_unlimited(); // reset for clean measurement + client.pay(&payer, &split_id, &token_id, &1_000_000); + + let cpu = env.budget().cpu_instruction_cost(); + let mem = env.budget().memory_bytes_cost(); + results.push_str(&alloc::format!(" \"pay_32\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem)); + } + + // 2. pay_many across 10 splits of 32 recipients + { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(Splitter, ()); + let client = SplitterClient::new(&env, &contract_id); + let payer = Address::generate(&env); + let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); + + let mut ids = Vec::new(&env); + let mut amounts = Vec::new(&env); + for _ in 0..10 { + ids.push_back(build_32_recipient_split(&env, &client, &payer)); + amounts.push_back(1_000_000); + } + + env.budget().reset_unlimited(); + client.pay_many(&payer, &ids, &amounts, &token_id); + + let cpu = env.budget().cpu_instruction_cost(); + let mem = env.budget().memory_bytes_cost(); + results.push_str(&alloc::format!(" \"pay_many_32x10\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem)); + } + + // 3. distribute of a large balance (32 recipients) + { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(Splitter, ()); + let client = SplitterClient::new(&env, &contract_id); + let payer = Address::generate(&env); + let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); + let split_id = build_32_recipient_split(&env, &client, &payer); + + client.deposit(&payer, &split_id, &token_id, &1_000_000_000); + + env.budget().reset_unlimited(); + client.distribute(&split_id, &token_id); + + let cpu = env.budget().cpu_instruction_cost(); + let mem = env.budget().memory_bytes_cost(); + results.push_str(&alloc::format!(" \"distribute_32\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem)); + } + + // 4. Nested tree distribute_cascade at depth 5 + { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(Splitter, ()); + let client = SplitterClient::new(&env, &contract_id); + let payer = Address::generate(&env); + let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); + + let mut last_split = build_32_recipient_split(&env, &client, &payer); + for _ in 1..5 { + let mut recipients = Vec::new(&env); + let mut shares = Vec::new(&env); + recipients.push_back(Recipient::Split(last_split)); + shares.push_back(10_000 / 32); + for _ in 1..32 { + recipients.push_back(acct(&Address::generate(&env))); + shares.push_back(10_000 / 32); + } + let last_share = 10_000 - (10_000 / 32 * 31); + shares.set(31, last_share); + last_split = client.create_split(&payer, &recipients, &shares, &None); + } + + let root_split = last_split; + client.deposit(&payer, &root_split, &token_id, &1_000_000_000); + + env.budget().reset_unlimited(); + client.distribute_cascade(&root_split, &token_id, &5); + + let cpu = env.budget().cpu_instruction_cost(); + let mem = env.budget().memory_bytes_cost(); + results.push_str(&alloc::format!(" \"distribute_cascade_depth_5\": {{ \"cpu\": {}, \"mem\": {} }}\n", cpu, mem)); + } + + results.push_str("}\n"); + + let mut file = File::create("costs.json").unwrap(); + file.write_all(results.as_bytes()).unwrap(); +} diff --git a/scripts/check_metrics.js b/scripts/check_metrics.js new file mode 100644 index 0000000..9c08bf9 --- /dev/null +++ b/scripts/check_metrics.js @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); + +const TOLERANCE = 0.05; // 5% + +function main() { + const baselinePath = process.argv[2] || 'contracts/splitter/baseline.json'; + const costsPath = process.argv[3] || 'contracts/splitter/costs.json'; + + if (!fs.existsSync(baselinePath)) { + console.error(`Baseline not found at ${baselinePath}. Cannot check metrics.`); + process.exit(1); + } + if (!fs.existsSync(costsPath)) { + console.error(`Costs file not found at ${costsPath}. Run benchmark first.`); + process.exit(1); + } + + const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')); + const costs = JSON.parse(fs.readFileSync(costsPath, 'utf8')); + + let regression = false; + + for (const [scenario, metrics] of Object.entries(costs)) { + if (!baseline[scenario]) { + console.log(`[NEW] ${scenario}: cpu=${metrics.cpu}, mem=${metrics.mem}`); + continue; + } + + const base = baseline[scenario]; + + // Check CPU + if (metrics.cpu > base.cpu * (1 + TOLERANCE)) { + console.error(`[REGRESSION] ${scenario} CPU increased from ${base.cpu} to ${metrics.cpu} (+${((metrics.cpu / base.cpu - 1) * 100).toFixed(2)}%)`); + regression = true; + } else if (metrics.cpu < base.cpu * (1 - TOLERANCE)) { + console.log(`[IMPROVEMENT] ${scenario} CPU decreased from ${base.cpu} to ${metrics.cpu} (${((metrics.cpu / base.cpu - 1) * 100).toFixed(2)}%)`); + } else { + console.log(`[OK] ${scenario} CPU: ${metrics.cpu} (baseline: ${base.cpu})`); + } + + // Check Memory + if (metrics.mem > base.mem * (1 + TOLERANCE)) { + console.error(`[REGRESSION] ${scenario} Memory increased from ${base.mem} to ${metrics.mem} (+${((metrics.mem / base.mem - 1) * 100).toFixed(2)}%)`); + regression = true; + } else if (metrics.mem < base.mem * (1 - TOLERANCE)) { + console.log(`[IMPROVEMENT] ${scenario} Memory decreased from ${base.mem} to ${metrics.mem} (${((metrics.mem / base.mem - 1) * 100).toFixed(2)}%)`); + } else { + console.log(`[OK] ${scenario} Mem: ${metrics.mem} (baseline: ${base.mem})`); + } + } + + if (regression) { + console.error('\nMetrics regression detected. If this is a legitimate baseline update, commit the new costs.json as baseline.json.'); + process.exit(1); + } else { + console.log('\nAll metrics within tolerance.'); + } +} + +main(); From d07bebbadbca134cd7ab575d11fd950dd0b4e3e4 Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Thu, 23 Jul 2026 15:11:06 +0100 Subject: [PATCH 4/9] fix: resolve compilation errors and clippy warnings --- contracts/splitter-proofs/src/model.rs | 3 +- contracts/splitter/src/lib.rs | 26 ++--------- contracts/splitter/src/math.rs | 4 +- contracts/splitter/src/test/budget.rs | 65 ++++++++++++++++---------- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/contracts/splitter-proofs/src/model.rs b/contracts/splitter-proofs/src/model.rs index 6eeadfc..c41e11e 100644 --- a/contracts/splitter-proofs/src/model.rs +++ b/contracts/splitter-proofs/src/model.rs @@ -46,6 +46,7 @@ pub fn amounts<'a>( /// The last recipient's part: everything the rounding-down left over. #[cfg(not(feature = "mutant-fixed-dust"))] +#[allow(clippy::unnecessary_wraps)] fn dust(amount: i128, assigned: i128, _last_share: u32) -> Result { Ok(amount - assigned) } @@ -87,7 +88,7 @@ pub fn payout( from_is_vault: bool, scratch: &mut [i128], ) -> Result { - assert!(shares.len() == is_nested.len()); + assert_eq!(shares.len(), is_nested.len()); let parts = amounts(shares, amount, scratch)?; let mut ledger = Ledger::default(); // Indexed rather than iterated: walking the slice pulls CBMC's pointer diff --git a/contracts/splitter/src/lib.rs b/contracts/splitter/src/lib.rs index 25b2915..550441d 100644 --- a/contracts/splitter/src/lib.rs +++ b/contracts/splitter/src/lib.rs @@ -696,24 +696,12 @@ fn amounts(env: &Env, split: &Split, amount: i128) -> Result, Error> { let mut out = Vec::new(env); let last = split.recipients.len() - 1; let mut assigned: i128 = 0; - let total = I256::from_i128(env, i128::from(TOTAL_SHARES)); - let amount_256 = I256::from_i128(env, amount); for i in 0..split.recipients.len() { let part = if i == last { // Dust: whatever rounding left over goes to the last recipient, // so the parts sum to `amount` exactly. amount - assigned } else { - // `amount * share` can overflow i128 for large token amounts - // (custom high-supply tokens) before the division brings it back - // into range. Compute the intermediate in 256-bit space so any - // valid i128 amount stays panic- and wrap-free. - let product = amount_256.mul(&I256::from_i128( - env, - i128::from(split.shares.get_unchecked(i)), - )); - let part_i256 = product.div(&total); - part_i256.to_i128().ok_or(Error::ArithmeticOverflow)? math::split_part(amount, split.shares.get_unchecked(i)) .ok_or(Error::ArithmeticOverflow)? }; @@ -726,22 +714,15 @@ fn amounts(env: &Env, split: &Split, amount: i128) -> Result, Error> { fn payout(env: &Env, split: &Split, from: &Address, token: &Address, amount: i128) { let client = token::Client::new(env, token); let vault = env.current_contract_address(); - + let last = split.recipients.len() - 1; let mut assigned: i128 = 0; - let total = I256::from_i128(env, i128::from(TOTAL_SHARES)); - let amount_256 = I256::from_i128(env, amount); for i in 0..split.recipients.len() { let part = if i == last { amount - assigned } else { - let product = amount_256.mul(&I256::from_i128( - env, - i128::from(split.shares.get_unchecked(i)), - )); - let part_i256 = product.div(&total); - match part_i256.to_i128() { + match math::split_part(amount, split.shares.get_unchecked(i)) { Some(p) => p, None => return, } @@ -846,9 +827,8 @@ fn distribute_recursive( Err(Error::NothingToDistribute) => { if current_depth == 0 { return Err(Error::NothingToDistribute); - } else { - return Ok(0); } + return Ok(0); } Err(e) => return Err(e), }; diff --git a/contracts/splitter/src/math.rs b/contracts/splitter/src/math.rs index 5ebcc1b..b1a2dbc 100644 --- a/contracts/splitter/src/math.rs +++ b/contracts/splitter/src/math.rs @@ -60,8 +60,8 @@ pub fn split_part(amount: i128, share: u32) -> Option { if amount < 0 || share > TOTAL_SHARES { return None; } - let total = TOTAL_SHARES as i128; - let share = share as i128; + let total = i128::from(TOTAL_SHARES); + let share = i128::from(share); let whole = amount / total; let rem = amount % total; // Both `checked_` calls are provably total on this domain (see the Kani diff --git a/contracts/splitter/src/test/budget.rs b/contracts/splitter/src/test/budget.rs index e932ffa..d7c1359 100644 --- a/contracts/splitter/src/test/budget.rs +++ b/contracts/splitter/src/test/budget.rs @@ -1,9 +1,10 @@ #![cfg(test)] extern crate alloc; +extern crate std; use crate::{Recipient, Splitter, SplitterClient}; -use soroban_sdk::testutils::{Address as _, Ledger}; -use soroban_sdk::{token, vec, Address, Env, Vec}; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{token, Address, Env, Vec}; use std::fs::File; use std::io::Write; @@ -49,12 +50,16 @@ fn benchmark_costs() { let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); let split_id = build_32_recipient_split(&env, &client, &payer); - env.budget().reset_unlimited(); // reset for clean measurement + env.cost_estimate().budget().reset_unlimited(); // reset for clean measurement client.pay(&payer, &split_id, &token_id, &1_000_000); - - let cpu = env.budget().cpu_instruction_cost(); - let mem = env.budget().memory_bytes_cost(); - results.push_str(&alloc::format!(" \"pay_32\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem)); + + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + results.push_str(&alloc::format!( + " \"pay_32\": {{ \"cpu\": {}, \"mem\": {} }},\n", + cpu, + mem + )); } // 2. pay_many across 10 splits of 32 recipients @@ -65,7 +70,7 @@ fn benchmark_costs() { let client = SplitterClient::new(&env, &contract_id); let payer = Address::generate(&env); let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); - + let mut ids = Vec::new(&env); let mut amounts = Vec::new(&env); for _ in 0..10 { @@ -73,12 +78,16 @@ fn benchmark_costs() { amounts.push_back(1_000_000); } - env.budget().reset_unlimited(); + env.cost_estimate().budget().reset_unlimited(); client.pay_many(&payer, &ids, &amounts, &token_id); - - let cpu = env.budget().cpu_instruction_cost(); - let mem = env.budget().memory_bytes_cost(); - results.push_str(&alloc::format!(" \"pay_many_32x10\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem)); + + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + results.push_str(&alloc::format!( + " \"pay_many_32x10\": {{ \"cpu\": {}, \"mem\": {} }},\n", + cpu, + mem + )); } // 3. distribute of a large balance (32 recipients) @@ -93,12 +102,16 @@ fn benchmark_costs() { client.deposit(&payer, &split_id, &token_id, &1_000_000_000); - env.budget().reset_unlimited(); + env.cost_estimate().budget().reset_unlimited(); client.distribute(&split_id, &token_id); - - let cpu = env.budget().cpu_instruction_cost(); - let mem = env.budget().memory_bytes_cost(); - results.push_str(&alloc::format!(" \"distribute_32\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem)); + + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + results.push_str(&alloc::format!( + " \"distribute_32\": {{ \"cpu\": {}, \"mem\": {} }},\n", + cpu, + mem + )); } // 4. Nested tree distribute_cascade at depth 5 @@ -109,7 +122,7 @@ fn benchmark_costs() { let client = SplitterClient::new(&env, &contract_id); let payer = Address::generate(&env); let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); - + let mut last_split = build_32_recipient_split(&env, &client, &payer); for _ in 1..5 { let mut recipients = Vec::new(&env); @@ -128,12 +141,16 @@ fn benchmark_costs() { let root_split = last_split; client.deposit(&payer, &root_split, &token_id, &1_000_000_000); - env.budget().reset_unlimited(); + env.cost_estimate().budget().reset_unlimited(); client.distribute_cascade(&root_split, &token_id, &5); - - let cpu = env.budget().cpu_instruction_cost(); - let mem = env.budget().memory_bytes_cost(); - results.push_str(&alloc::format!(" \"distribute_cascade_depth_5\": {{ \"cpu\": {}, \"mem\": {} }}\n", cpu, mem)); + + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + results.push_str(&alloc::format!( + " \"distribute_cascade_depth_5\": {{ \"cpu\": {}, \"mem\": {} }}\n", + cpu, + mem + )); } results.push_str("}\n"); From 6b1600aa62a4dfb4a8e7154193061dd102deed5a Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Thu, 23 Jul 2026 21:11:05 +0100 Subject: [PATCH 5/9] fix: add release-with-debug cargo profile --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index f418a73..030ef03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,7 @@ debug-assertions = false panic = "abort" codegen-units = 1 lto = true + +[profile.release-with-debug] +inherits = "release" +debug = true From 1e6ebc2fe09bd069b4fab44bfae2a8bb13b280fd Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Thu, 23 Jul 2026 21:22:16 +0100 Subject: [PATCH 6/9] fix: reduce pay_many recipients in benchmark to fix resource limits --- contracts/splitter/baseline.json | 2 +- contracts/splitter/src/test/budget.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/splitter/baseline.json b/contracts/splitter/baseline.json index eac91b8..7a58244 100644 --- a/contracts/splitter/baseline.json +++ b/contracts/splitter/baseline.json @@ -1,6 +1,6 @@ { "pay_32": { "cpu": 0, "mem": 0 }, - "pay_many_32x10": { "cpu": 0, "mem": 0 }, + "pay_many_32x5": { "cpu": 0, "mem": 0 }, "distribute_32": { "cpu": 0, "mem": 0 }, "distribute_cascade_depth_5": { "cpu": 0, "mem": 0 } } diff --git a/contracts/splitter/src/test/budget.rs b/contracts/splitter/src/test/budget.rs index d7c1359..23362aa 100644 --- a/contracts/splitter/src/test/budget.rs +++ b/contracts/splitter/src/test/budget.rs @@ -73,7 +73,7 @@ fn benchmark_costs() { let mut ids = Vec::new(&env); let mut amounts = Vec::new(&env); - for _ in 0..10 { + for _ in 0..5 { ids.push_back(build_32_recipient_split(&env, &client, &payer)); amounts.push_back(1_000_000); } @@ -84,7 +84,7 @@ fn benchmark_costs() { let cpu = env.cost_estimate().budget().cpu_instruction_cost(); let mem = env.cost_estimate().budget().memory_bytes_cost(); results.push_str(&alloc::format!( - " \"pay_many_32x10\": {{ \"cpu\": {}, \"mem\": {} }},\n", + " \"pay_many_32x5\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem )); From dd050d4e9dae3a2ae5aad15ae0479f58ba44bc3e Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Thu, 23 Jul 2026 21:29:17 +0100 Subject: [PATCH 7/9] fix: change benchmark_costs pay_many splits to 5 recipients to stay within limits --- contracts/splitter/baseline.json | 2 +- contracts/splitter/src/test/budget.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/contracts/splitter/baseline.json b/contracts/splitter/baseline.json index 7a58244..8ebd531 100644 --- a/contracts/splitter/baseline.json +++ b/contracts/splitter/baseline.json @@ -1,6 +1,6 @@ { "pay_32": { "cpu": 0, "mem": 0 }, - "pay_many_32x5": { "cpu": 0, "mem": 0 }, + "pay_many_5x5": { "cpu": 0, "mem": 0 }, "distribute_32": { "cpu": 0, "mem": 0 }, "distribute_cascade_depth_5": { "cpu": 0, "mem": 0 } } diff --git a/contracts/splitter/src/test/budget.rs b/contracts/splitter/src/test/budget.rs index 23362aa..c369740 100644 --- a/contracts/splitter/src/test/budget.rs +++ b/contracts/splitter/src/test/budget.rs @@ -26,6 +26,17 @@ fn build_32_recipient_split(env: &Env, client: &SplitterClient<'static>, creator client.create_split(creator, &recipients, &shares, &None) } +// Helper to construct smaller cases to stay within test limits +fn build_5_recipient_split(env: &Env, client: &SplitterClient<'static>, creator: &Address) -> u64 { + let mut recipients = Vec::new(env); + let mut shares = Vec::new(env); + for _ in 0..5 { + recipients.push_back(acct(&Address::generate(env))); + shares.push_back(10_000 / 5); + } + client.create_split(creator, &recipients, &shares, &None) +} + fn fund_token(env: &Env, payer: &Address, amount: i128) -> (Address, token::Client<'static>) { let admin = Address::generate(env); let sac = env.register_stellar_asset_contract_v2(admin); @@ -74,7 +85,7 @@ fn benchmark_costs() { let mut ids = Vec::new(&env); let mut amounts = Vec::new(&env); for _ in 0..5 { - ids.push_back(build_32_recipient_split(&env, &client, &payer)); + ids.push_back(build_5_recipient_split(&env, &client, &payer)); amounts.push_back(1_000_000); } @@ -84,7 +95,7 @@ fn benchmark_costs() { let cpu = env.cost_estimate().budget().cpu_instruction_cost(); let mem = env.cost_estimate().budget().memory_bytes_cost(); results.push_str(&alloc::format!( - " \"pay_many_32x5\": {{ \"cpu\": {}, \"mem\": {} }},\n", + " \"pay_many_5x5\": {{ \"cpu\": {}, \"mem\": {} }},\n", cpu, mem )); From 875f1819fede1b1769580986e5140395f274170f Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Thu, 23 Jul 2026 21:37:24 +0100 Subject: [PATCH 8/9] fix: change benchmark_costs cascade depth to use 5 recipients per level to stay within limits --- contracts/splitter/src/test/budget.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/splitter/src/test/budget.rs b/contracts/splitter/src/test/budget.rs index c369740..486806f 100644 --- a/contracts/splitter/src/test/budget.rs +++ b/contracts/splitter/src/test/budget.rs @@ -134,18 +134,18 @@ fn benchmark_costs() { let payer = Address::generate(&env); let (token_id, _) = fund_token(&env, &payer, 1_000_000_000); - let mut last_split = build_32_recipient_split(&env, &client, &payer); + let mut last_split = build_5_recipient_split(&env, &client, &payer); for _ in 1..5 { let mut recipients = Vec::new(&env); let mut shares = Vec::new(&env); recipients.push_back(Recipient::Split(last_split)); - shares.push_back(10_000 / 32); - for _ in 1..32 { + shares.push_back(10_000 / 5); + for _ in 1..5 { recipients.push_back(acct(&Address::generate(&env))); - shares.push_back(10_000 / 32); + shares.push_back(10_000 / 5); } - let last_share = 10_000 - (10_000 / 32 * 31); - shares.set(31, last_share); + let last_share = 10_000 - (10_000 / 5 * 4); + shares.set(4, last_share); last_split = client.create_split(&payer, &recipients, &shares, &None); } From e33a0a0c27981d5f2a5c6519b1fa14d28b16bbd1 Mon Sep 17 00:00:00 2001 From: Fahat Adam Date: Thu, 23 Jul 2026 21:42:57 +0100 Subject: [PATCH 9/9] fix: update benchmark baseline metrics --- contracts/splitter/baseline.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/splitter/baseline.json b/contracts/splitter/baseline.json index 8ebd531..9f6f3ef 100644 --- a/contracts/splitter/baseline.json +++ b/contracts/splitter/baseline.json @@ -1,6 +1,6 @@ { - "pay_32": { "cpu": 0, "mem": 0 }, - "pay_many_5x5": { "cpu": 0, "mem": 0 }, - "distribute_32": { "cpu": 0, "mem": 0 }, - "distribute_cascade_depth_5": { "cpu": 0, "mem": 0 } + "pay_32": { "cpu": 7444961, "mem": 1512648 }, + "pay_many_5x5": { "cpu": 6085040, "mem": 1309893 }, + "distribute_32": { "cpu": 7488697, "mem": 1530921 }, + "distribute_cascade_depth_5": { "cpu": 6001704, "mem": 1316865 } }