Starter source
Inspect the files included in the website starter. The server checks the delegate’s signature and devnet eligibility before returning protected content.
README.md
# Connect an app to Solbound
Solbound is a public Solana devnet registry. A vault can delegate verification to an everyday wallet across all integrated apps, or restrict it to one HTTPS origin. The app chooses its own access rule. A wallet-wide delegation works without owning a token.
## Add the SDK to your website
1. In **For Websites**, enter your HTTPS origin and protected route. Choose a wallet link, NFT, collection or SPL balance rule. Generate settings, download `solbound-sdk-0.1.0.tgz`, and save `solbound.config.json`. Place both files beside your app’s `package.json`.
2. With Node.js 24, run `npm install ./solbound-sdk-0.1.0.tgz`. Choose the Next.js, Node HTTP or existing wallet login example. The examples archive and release checksums are available alongside the SDK download.
3. Use `createSolbound` from `@solbound-io/sdk` to configure the server’s policy. On an existing signed-in session, call `solbound.verify({authenticatedDelegate: session.wallet})` and require `result.eligible` before returning private content. For new sign-in routes, use `createSolboundHandlers` from `@solbound-io/sdk/server` and wrap the private response with `gate.protect`. The optional `SolboundSignIn` component from `@solbound-io/sdk/react` connects a message-signing wallet to the same-origin endpoints.
4. Choose **Run setup check** after serving the SDK at `/api/solbound`. The dashboard checks the public health report against your rule, audience and the expected devnet program. Alternatively, run `npx --no-install solbound doctor --config solbound.config.json --json` locally and paste its JSON report. A local report describes that installation. Test the hosted resource separately.
5. Create a devnet delegation from your vault to your everyday wallet in My Links. Sign in to the protected page with the everyday wallet. Revoke the last matching grant, wait for finalization and request the same page again. It should now deny access.
The SDK’s default one-process store holds expiring sessions and challenges. Multiple instances require shared expiring storage with atomic challenge consumption, or an existing server-verified wallet login. Every protected response needs a fresh eligibility check. Keep private RPC credentials on the server.
## Run the advanced reference server
The standalone starter is a separate example with its own environment file. Extract its archive and create `solbound-pilot.env` beside `package.json`. For a wallet-wide rule, use the following settings with your own HTTPS origin.
```dotenv
SOLBOUND_ORIGIN=https://gallery.example
SOLBOUND_PATH=/members
SOLBOUND_RULE=wallet
```
With Node 24 installed, run `npm ci` followed by `npm run pilot`. Open the printed loopback URL. Your wallet signs for the local site while the verifier checks grants for the configured HTTPS audience. For asset rules, set `SOLBOUND_ADDRESS` to the trusted mint and use `SOLBOUND_RULE=mint`, `collection` or `balance`. For a balance threshold, set `SOLBOUND_MINIMUM` in whole base units.
## Test with Mudwater
Run `npm run pilot:mudwater` from the project root. Open `http://127.0.0.1:4174/mudwater/`. The preset models `https://wagmimail.io/mudwater` and checks for a wallet-wide delegation. You can test with a vault that holds no NFT. To change the rule, edit your local environment file and use `npm run pilot`.
## Integrate your backend
The reference server in `partner/server.mjs` shows the complete flow. Issue a single-use challenge for your app's exact origin and session. Authenticate the delegate's signature. Keep the authenticated delegate separate from the vault it represents. Gate each protected response or action on a fresh check.
- Use `checkDelegation(client,{origin,authenticatedDelegate,vault})` for a wallet-wide relationship. Add `asset` for an exact mint or asset address. This checks the registry relationship only. It gives no transfer or spending authority.
- Use `findHolderEligibility(client,{origin,authenticatedDelegate,rule,address,minimum})` for automatic discovery. The server chooses the rule. In wallet mode an asset address is unnecessary. In balance mode supply the trusted token mint and a positive minimum in whole base units as a string. In NFT modes the server verifies classic SPL ownership and, when applicable, verified Metaplex collection membership.
- Use `checkHolderEligibility` for an explicitly selected vault. Pass the trusted rule and address as shown in the server. For a collection rule the holder may suggest an NFT mint, and the server checks it against the configured collection. Core, compressed NFTs and Token-2022 require additional ownership adapters.
- The sample limits automatic discovery to 40 records per audience filter, two vault searches for collections and six candidate holdings. Manual vault lookup is available. Balance checks use up to 90 distinct token accounts at a common finalized slot. An unconfirmed balance denies access.
Set `SOLBOUND_HOLDER_MESSAGE` for the example's protected content. Set `SOLBOUND_RPC` for a trusted HTTPS devnet RPC. The live program ID and code digest are checked. Optionally set `SOLBOUND_STUDIO` to the trusted Solbound website origin. A local origin can be `http://127.0.0.1:3000`. The gate links to that homepage with the proposed delegate, website and scope in the URL fragment. If you omit the origin, members can open My Links on their trusted Solbound site.
For a devnet integration on your own domain, run `node --env-file=solbound-pilot.env partner/server.mjs` behind HTTPS. Forward the configured route and its subpaths, preserve Host, and bypass caches. A single server process owns the pilot's expiring sessions and challenges. Multiple instances need shared storage and atomic challenge consumption.
Older configurations that only specified a mint or collection need `SOLBOUND_ORIGIN` and should set `SOLBOUND_PATH` to their original route. Previously signed domain-scoped grants remain valid only for their original domain.
package.json
{
"name": "solbound-app-integration",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=24"
},
"scripts": {
"pilot": "node --env-file-if-exists=solmate-pilot.env --env-file-if-exists=solbound-pilot.env partner/server.mjs --local",
"pilot:mudwater": "node --env-file=partner/mudwater.env.example partner/server.mjs --local"
},
"dependencies": {
"@metaplex-foundation/mpl-token-metadata": "3.4.0",
"@metaplex-foundation/umi": "1.4.1",
"@solana/web3.js": "1.98.4",
"@wallet-standard/app": "1.1.1",
"buffer": "6.0.3"
}
}
package-lock.json
{
"name": "solbound-app-integration",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "solbound-app-integration",
"version": "0.1.0",
"dependencies": {
"@metaplex-foundation/mpl-token-metadata": "3.4.0",
"@metaplex-foundation/umi": "1.4.1",
"@solana/web3.js": "1.98.4",
"@wallet-standard/app": "1.1.1",
"buffer": "6.0.3"
},
"engines": {
"node": ">=24"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz",
"integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@metaplex-foundation/mpl-token-metadata": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-3.4.0.tgz",
"integrity": "sha512-AxBAYCK73JWxY3g9//z/C9krkR0t1orXZDknUPS4+GjwGH2vgPfsk04yfZ31Htka2AdS9YE/3wH7sMUBHKn9Rg==",
"license": "Apache-2.0",
"dependencies": {
"@metaplex-foundation/mpl-toolbox": "^0.10.0"
},
"peerDependencies": {
"@metaplex-foundation/umi": ">= 0.8.2 <= 1"
}
},
"node_modules/@metaplex-foundation/mpl-toolbox": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-toolbox/-/mpl-toolbox-0.10.0.tgz",
"integrity": "sha512-84KD1L5cFyw5xnntHwL4uPwfcrkKSiwuDeypiVr92qCUFuF3ZENa2zlFVPu+pQcjTlod2LmEX3MhBmNjRMpdKg==",
"license": "Apache-2.0",
"peerDependencies": {
"@metaplex-foundation/umi": ">= 0.8.2 <= 1"
}
},
"node_modules/@metaplex-foundation/umi": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi/-/umi-1.4.1.tgz",
"integrity": "sha512-GGdmKsZq8nGInhjk8jpW4h4pNX8eZgTChnosvBmX/4lTznu+nOJuIsgO9r7LuUkR8zaSfjUheC/T5e7AtRku+g==",
"license": "MIT",
"dependencies": {
"@metaplex-foundation/umi-options": "^1.4.1",
"@metaplex-foundation/umi-public-keys": "^1.4.1",
"@metaplex-foundation/umi-serializers": "^1.4.1"
}
},
"node_modules/@metaplex-foundation/umi-options": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-options/-/umi-options-1.6.0.tgz",
"integrity": "sha512-kFamaTg/MDXKLP9XsSvIw66hQ0fVOR4a63iaZtXJCoFuCoxTRQZAHu7S6tVDbeIIGaqB0gO6qYX0RSjxluXhQQ==",
"license": "MIT"
},
"node_modules/@metaplex-foundation/umi-public-keys": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-public-keys/-/umi-public-keys-1.6.0.tgz",
"integrity": "sha512-RzTi8WkjTOB40LPL/4162bSb6p6vknlJj56JLaiMXsfzkshhxRb+LpuWjI6zm/IGXlpGaFLQrkIos0BT6FzBAA==",
"license": "MIT",
"dependencies": {
"@metaplex-foundation/umi-serializers-encodings": "^1.6.0"
}
},
"node_modules/@metaplex-foundation/umi-serializers": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers/-/umi-serializers-1.6.0.tgz",
"integrity": "sha512-O0lPPjJGdOx5uxlE/BASErtLonD7BoMi5a0QIazRGAJzaApq/0Nup4QT9oGOPuhE1zncxhhK/rTAfNAqYoA2ng==",
"license": "MIT",
"dependencies": {
"@metaplex-foundation/umi-options": "^1.6.0",
"@metaplex-foundation/umi-public-keys": "^1.6.0",
"@metaplex-foundation/umi-serializers-core": "^1.6.0",
"@metaplex-foundation/umi-serializers-encodings": "^1.6.0",
"@metaplex-foundation/umi-serializers-numbers": "^1.6.0"
}
},
"node_modules/@metaplex-foundation/umi-serializers-core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-core/-/umi-serializers-core-1.6.0.tgz",
"integrity": "sha512-5AJvEK4j9oAjRtrjBcNdDYzY7opYVKVlblFwcMp0GHM+xB3/OMW6UpeuZPk6sHDbhQArFpFycpBqRT9heE6w4Q==",
"license": "MIT"
},
"node_modules/@metaplex-foundation/umi-serializers-encodings": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-encodings/-/umi-serializers-encodings-1.6.0.tgz",
"integrity": "sha512-ZZSnbsVJOABJcopQzTTU6Ek99O9tJItkoq+nb/EtDOkzQGhtfQjxpgnoKbgoGnguwbQNuvgM41sPRHHmL0XMig==",
"license": "MIT",
"dependencies": {
"@metaplex-foundation/umi-serializers-core": "^1.6.0"
}
},
"node_modules/@metaplex-foundation/umi-serializers-numbers": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-numbers/-/umi-serializers-numbers-1.6.0.tgz",
"integrity": "sha512-pr48YJoCv6sow/j24jelUfq0zkxK5FQeQEG9FFycRdL/SXVBV6A+dOCVwt7lr7FPTHoftjoJcFCgBufI6mbbbw==",
"license": "MIT",
"dependencies": {
"@metaplex-foundation/umi-serializers-core": "^1.6.0"
}
},
"node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@solana/buffer-layout": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz",
"integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==",
"license": "MIT",
"dependencies": {
"buffer": "~6.0.3"
},
"engines": {
"node": ">=5.10"
}
},
"node_modules/@solana/codecs-core": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz",
"integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==",
"license": "MIT",
"dependencies": {
"@solana/errors": "2.3.0"
},
"engines": {
"node": ">=20.18.0"
},
"peerDependencies": {
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/codecs-numbers": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz",
"integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.3.0",
"@solana/errors": "2.3.0"
},
"engines": {
"node": ">=20.18.0"
},
"peerDependencies": {
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/errors": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz",
"integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==",
"license": "MIT",
"dependencies": {
"chalk": "^5.4.1",
"commander": "^14.0.0"
},
"bin": {
"errors": "bin/cli.mjs"
},
"engines": {
"node": ">=20.18.0"
},
"peerDependencies": {
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/web3.js": {
"version": "1.98.4",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz",
"integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
"@noble/curves": "^1.4.2",
"@noble/hashes": "^1.4.0",
"@solana/buffer-layout": "^4.0.1",
"@solana/codecs-numbers": "^2.1.0",
"agentkeepalive": "^4.5.0",
"bn.js": "^5.2.1",
"borsh": "^0.7.0",
"bs58": "^4.0.1",
"buffer": "6.0.3",
"fast-stable-stringify": "^1.0.0",
"jayson": "^4.1.1",
"node-fetch": "^2.7.0",
"rpc-websockets": "^9.0.2",
"superstruct": "^2.0.2"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.24.tgz",
"integrity": "sha512-6tcXo4kA8HgGO7AVmawB57RYi/piBPYVHVcO6dXjX+SZw8GoHmi262sp4v2Ha2kNP9Cx84qei27yT+fjS1lxgg==",
"license": "Apache-2.0"
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"aix"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"sunos"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"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"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@wallet-standard/app": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@wallet-standard/app/-/app-1.1.1.tgz",
"integrity": "sha512-WDGwoByhP5gwHH01r5EaLgQdLVkACPCdOMQhmhn8rsm10h/siSgTorShzBxrn0ExSPof+Lu+C3TfgqBrPa1xoQ==",
"license": "Apache-2.0",
"dependencies": {
"@wallet-standard/base": "^1.1.1"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@wallet-standard/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.1.tgz",
"integrity": "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=22"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/base-x": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
"integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"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/bn.js": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz",
"integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==",
"license": "MIT"
},
"node_modules/borsh": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
"integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
"license": "Apache-2.0",
"dependencies": {
"bn.js": "^5.2.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
}
},
"node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"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/bufferutil": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.2.0.tgz",
"integrity": "sha512-gyy58jET9kst2IGyBKb96a4Y2bPGTHXGyCd0/1eqIV3hr1Sz3VxtA8n7bXh8fLIuE2K6Z0Upax5a5Msx+HU1IQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/delay": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
"license": "MIT",
"dependencies": {
"es6-promise": "^4.0.3"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/eyes": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
"engines": {
"node": "> 0.1.90"
}
},
"node_modules/fast-stable-stringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz",
"integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==",
"license": "MIT"
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.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/isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/jayson": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz",
"integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==",
"license": "MIT",
"dependencies": {
"@types/connect": "^3.4.33",
"@types/node": "^12.12.54",
"@types/ws": "^7.4.4",
"commander": "^2.20.3",
"delay": "^5.0.0",
"es6-promisify": "^5.0.0",
"eyes": "^0.1.8",
"isomorphic-ws": "^4.0.1",
"json-stringify-safe": "^5.0.1",
"stream-json": "^1.9.1",
"uuid": "^8.3.2",
"ws": "^7.5.10"
},
"bin": {
"jayson": "bin/jayson.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jayson/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/rpc-websockets": {
"version": "9.3.9",
"resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz",
"integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==",
"license": "LGPL-3.0-only",
"dependencies": {
"@swc/helpers": "^0.5.11",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.2.2",
"buffer": "^6.0.3",
"eventemitter3": "^5.0.1",
"uuid": "^14.0.0",
"ws": "^8.5.0"
},
"funding": {
"type": "paypal",
"url": "https://paypal.me/kozjak"
},
"optionalDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^6.0.0"
}
},
"node_modules/rpc-websockets/node_modules/@types/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/rpc-websockets/node_modules/utf-8-validate": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz",
"integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/rpc-websockets/node_modules/uuid": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz",
"integrity": "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/rpc-websockets/node_modules/ws": {
"version": "8.22.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.2.tgz",
"integrity": "sha512-/rEZ2ejown2N5gxVTHUu9bVVC7ttu0/Tgzf5hbTOixR3Fs7mbbXgn2DbdbcKgJOmpQMtrIUWivtYuVMnp/W/yA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"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/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/superstruct": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
"integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/text-encoding-utf-8": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
"integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"license": "Apache-2.0",
"peer": true,
"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/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "7.5.13",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
"integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
partner/server.mjs
import { randomBytes, createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { pathToFileURL } from 'node:url';
import { DEVNET_PROGRAM_ID, isAddress, openDevnet } from '../lib/registry.mjs';
import { appPath, siteOrigin } from '../lib/partners.mjs';
import { authenticateChallenge, checkHolderEligibility, findHolderEligibility, issueChallenge, validateRule } from '../tools/partner-verifier.mjs';
const files = new Map([
['/', ['gate.html', 'text/html; charset=utf-8']],
['/gate.js', ['gate.js', 'text/javascript; charset=utf-8']],
['/gate.css', ['gate.css', 'text/css; charset=utf-8']],
['/browser-wallets.mjs', ['../lib/browser-wallets.mjs', 'text/javascript; charset=utf-8']],
['/wallet-standard.js', [new URL('wallets.js',import.meta.resolve('@wallet-standard/app')).href,'text/javascript; charset=utf-8']],
]);
class HttpError extends Error {
constructor(status, message) { super(message); this.status = status; }
}
function send(res, status, payload) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(payload));
}
async function readInput(req) {
if (req.headers['content-type']?.split(';')[0] !== 'application/json') throw new HttpError(415, 'Send JSON.');
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > 4096) throw new HttpError(413, 'Request is too large.');
chunks.push(chunk);
}
try {
const data = JSON.parse(Buffer.concat(chunks).toString());
if (data && typeof data === 'object' && !Array.isArray(data)) return data;
} catch {}
throw new HttpError(400, 'Invalid request.');
}
// One authoritative process for the pilot. Restarting it signs everyone out.
function expiringStore() {
const entries = new Map();
return {
get(id) {
const item = entries.get(id);
if (item && item.expiresAt > Date.now()) return item;
entries.delete(id);
},
save(id, item) {
if (entries.size >= 2048) {
for (const [key, row] of entries) if (row.expiresAt <= Date.now()) entries.delete(key);
if (entries.size >= 2048) throw new HttpError(429, 'The pilot is busy. Try again shortly.');
}
entries.set(id, item);
},
consume(id, sessionId, expiry) {
const item = this.get(id);
return !!item && item.sessionId === sessionId && item.expiresAt === expiry && entries.delete(id);
},
delete(id) { entries.delete(id); },
};
}
export function createGate({ origin, signInOrigin=origin, path='/access', rule='wallet', address, minimum, studio, holderMessage }, {check, find}) {
if (siteOrigin(origin)!==origin) throw Error('Configure the HTTPS origin of your app.');
const base=appPath(path).replace(/\/$/,'');
const local = /^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(signInOrigin);
if (!local && signInOrigin !== origin) throw Error('The sign-in origin must match the configured app.');
validateRule(rule,address,minimum);
if (studio && !/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(studio) && siteOrigin(studio)!==studio) throw Error('Configure a trusted HTTPS Solbound origin or a loopback origin.');
const instance=createHash('sha256').update(origin+base).digest('hex').slice(0,16);
const cookie = (local ? '' : '__Host-') + 'solmate_' + instance;
const sessions = expiringStore(), challenges = expiringStore();
const args = { origin, signInOrigin, store: challenges };
const newId = () => randomBytes(32).toString('hex');
const setCookie = (res, value, age) => res.setHeader('Set-Cookie', cookie + '=' + value + '; HttpOnly; SameSite=Strict; Path=/; Max-Age=' + age + (local ? '' : '; Secure'));
return createServer(async (req, res) => {
res.setHeader('Cache-Control', 'private, no-store');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'no-referrer');
res.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'");
try {
if (req.headers.host !== new URL(signInOrigin).host) throw new HttpError(403, 'Open the configured partner website.');
const path = new URL(req.url, signInOrigin).pathname;
if (req.method === 'GET' && path === base) {
res.writeHead(308,{Location:base+'/'}); return res.end();
}
if (req.method === 'GET' && path.startsWith(base+'/') && files.has(path.slice(base.length))) {
const [name, type] = files.get(path.slice(base.length));
const body = await readFile(new URL(name, import.meta.url));
res.writeHead(200, { 'Content-Type': type }); return res.end(body);
}
if (req.method === 'GET' && path === base + '/settings') return send(res, 200, { origin, rule, address, minimum, studio });
if (req.method !== 'POST' || !path.startsWith(base + '/api/')) throw new HttpError(404, 'Page unavailable.');
if (req.headers.origin !== signInOrigin) throw new HttpError(403, 'Open the configured partner website.');
const data = await readInput(req);
const sessionId = (req.headers.cookie || '').split(';').map(s => s.trim()).find(s => s.startsWith(cookie + '='))?.slice(cookie.length + 1);
if (path === base + '/api/sign-out') {
sessions.delete(sessionId); setCookie(res, '', 0); return send(res, 200, {});
}
if (path === base + '/api/challenge') {
if (!isAddress(data.delegate)) throw new HttpError(400, 'Connect an everyday Solana wallet.');
const id = newId();
let challenge;
try { challenge = await issueChallenge({ ...args, sessionId: id, delegate: data.delegate }); }
catch (e) { if (e instanceof HttpError) throw e; throw new HttpError(400, 'Choose a wallet that can sign a message.'); }
sessions.delete(sessionId);
setCookie(res, id, 180);
return send(res, 200, challenge);
}
if (path === base + '/api/sign-in') {
let who;
try { who = await authenticateChallenge({ ...args, sessionId, id: data.id, signature: data.signature }); }
catch { throw new HttpError(401, 'Sign-in expired or the signature did not match. Sign in again.'); }
const id = newId();
sessions.save(id, { delegate: who, expiresAt: Date.now() + 5 * 60_000 });
setCookie(res, id, 300);
return send(res, 200, { delegate: who });
}
if (path !== base + '/api/holder') throw new HttpError(404, 'Page unavailable.');
const session = sessions.get(sessionId);
if (!session) throw new HttpError(401, 'Sign in with your everyday wallet first.');
const identity = { authenticatedDelegate: session.delegate, origin };
let result;
if (data.vault === undefined && data.mint === undefined) {
result = await find({ ...identity, rule, address, minimum });
} else {
const mint = rule === 'collection' ? data.mint : address;
if (!isAddress(data.vault) || (rule!=='wallet' && !isAddress(mint))) throw new HttpError(400, 'Check the public vault address and any required mint.');
result = await check({ ...identity, vault: data.vault, rule, minimum, qualifyingMint: mint, ...(rule === 'collection' ? { collection: address } : {}) });
}
if (!result.eligible) throw new HttpError(403, result.reason);
return send(res, 200, { content: holderMessage || 'Your delegation has unlocked this page.', vault:result.vault, slot: result.slot });
} catch (e) {
send(res, e instanceof HttpError ? e.status : 503, { error: e instanceof HttpError ? e.message : 'Verification is unavailable. Please try again shortly.' });
}
});
}
export function readPilotSettings(env) {
const setting = name => env['SOLBOUND_' + name] ?? env['SOLMATE_' + name];
return {
origin:siteOrigin(setting('ORIGIN')), path:appPath(setting('PATH')),
rpc:setting('RPC') || undefined, rule:setting('RULE'), address:setting('ADDRESS'),
minimum:setting('MINIMUM'), studio:setting('STUDIO'), holderMessage:setting('HOLDER_MESSAGE'),
};
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const port = Number(process.env.PORT || 4174);
const {rpc,...config} = readPilotSettings(process.env);
const signInOrigin = process.argv.includes('--local') ? 'http://127.0.0.1:' + port : config.origin;
const client = openDevnet(DEVNET_PROGRAM_ID, rpc);
const server = createGate({ ...config, signInOrigin },
{check: args => checkHolderEligibility(client, args), find: args => findHolderEligibility(client, args)});
server.listen(port, '127.0.0.1', () => console.log('Solbound devnet integration at ' + signInOrigin + config.path.replace(/\/$/,'') + '/'));
}
partner/mudwater.env.example
SOLBOUND_ORIGIN=https://wagmimail.io
SOLBOUND_PATH=/mudwater
SOLBOUND_RULE=wallet
partner/gate.html
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Delegated access · Solbound</title><link rel="stylesheet" href="./gate.css">
<script type="module" src="./gate.js"></script></head>
<body><main><header><span id="site">Solbound</span><small>Solana Devnet</small></header>
<article><p class="kicker">Delegated access</p><h1>Use your everyday wallet.</h1>
<p>Sign in to use the permission your vault granted on Solana devnet.</p>
<form id="gate">
<label for="wallet">Everyday wallet</label>
<select id="wallet"><option value="phantom">Phantom</option><option value="solflare">Solflare</option></select>
<details id="manual"><summary>Look up a specific vault</summary>
<label for="vault">Vault public address</label>
<input id="vault" autocomplete="off" spellcheck="false" placeholder="Vault that issued your delegation">
<div id="candidate" hidden><label for="mint">Your NFT’s mint address</label>
<input id="mint" autocomplete="off" spellcheck="false" placeholder="An NFT from the eligible collection"></div></details>
<button id="verify" disabled>Verify with Solbound</button>
</form>
<p id="status" role="status" aria-live="polite">Loading the holder rule…</p>
<a id="grant" hidden target="_blank" rel="noopener noreferrer">Create a Solbound delegation ↗</a>
<section id="holder" hidden><p class="kicker">Holder access</p><h2>Access verified</h2><p id="content"></p></section>
<button id="leave" class="quiet" hidden>Sign out</button>
</article><footer>Holder verification by solbound.io · WAGMI Labs LLC</footer></main></body></html>
partner/gate.css
:root{color-scheme:dark;font:16px/1.5 system-ui,sans-serif;color:#f5f2ff;background:#111019}
*{box-sizing:border-box}body{margin:0;min-height:100svh;background:radial-gradient(ellipse at 75% 5%,#323048 0,transparent 54%)}
main{max-width:35rem;margin:auto;padding:2rem 1.1rem}header,footer{padding:.7rem .2rem;color:#bfb9ce}
header{display:flex;justify-content:space-between;align-items:center;font-weight:650}small{font-weight:500;color:#abebd7}
article{margin:2rem 0 1rem;padding:clamp(1.4rem,5vw,3rem);background:#21202dcf;border:1px solid #ffffff24;border-radius:1.7rem;box-shadow:0 1.2rem 4rem #08071155}
h1,h2,p{margin:0 0 .8rem}h1{font-size:clamp(2.2rem,6vw,3.6rem);line-height:1.04;letter-spacing:-.065em}
.kicker{font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;color:#a2ebd6}article>p,footer{color:#c3bfd1}
label{display:block;font-size:.85rem;font-weight:600;margin:1.15rem 0 .35rem}
details{margin-top:1rem}summary{cursor:pointer;color:#c5bfda;font-size:.9rem}
input,select,button{font:inherit;border-radius:.7rem}input,select{width:100%;border:1px solid #ffffff35;background:#181720;color:inherit;padding:.72rem}
button{padding:.75rem 1.15rem;font-weight:650;background:#bfaff9;color:#211932;border:0;cursor:pointer}form button{margin:1.4rem 0 .8rem}button:disabled{opacity:.55;cursor:wait}
a,.quiet{color:#a5ead5}.quiet{background:transparent;padding:.6rem 0}section{border-top:1px solid #ffffff22;margin-top:1.2rem;padding-top:1.2rem}
footer,#status{font-size:.85rem}#status{overflow-wrap:anywhere}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid #a5ead5;outline-offset:3px}
partner/gate.js
import {getWallets} from './wallet-standard.js';
import {openWallet,walletChoices} from './browser-wallets.mjs';
const el = id => document.getElementById(id);
const base = new URL('./',import.meta.url).pathname;
let settings, connected, wallet, generation = 0, unsubscribe;
async function post(action, body) {
const response = await fetch(base + 'api/' + action, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) throw Object.assign(new Error(result.error), { status: response.status });
return result;
}
function clearContent() {
el('holder').hidden = true;
el('content').textContent = '';
}
function grantLink() {
if (!connected || !settings?.studio) { el('grant').hidden = true; return; }
const mint = settings.address;
const link = new URL('/', settings.studio);
const scope = settings.rule === 'wallet' ? 'wallet' : settings.rule === 'collection' ? 'collection' : 'asset';
link.hash = new URLSearchParams({ flow:'delegate', scope, site:settings.origin, delegate:connected, ...(mint ? {mint} : {}) });
el('grant').href = link.href;
el('grant').hidden = false;
}
async function signOut() {
generation++;
unsubscribe?.(); unsubscribe = null;
const previous = wallet;
connected = null; wallet = null;
clearContent(); grantLink();
el('leave').hidden = true; el('wallet').disabled = false;
el('status').textContent = 'Signed out.';
try { await post('sign-out', {}); } catch {}
try { await previous?.disconnect?.(); } catch {}
}
el('leave').addEventListener('click', signOut);
for (const field of ['vault', 'mint']) el(field).addEventListener('input', () => { clearContent(); grantLink(); });
el('gate').addEventListener('submit', async event => {
event.preventDefault();
const attempt = ++generation;
clearContent();
el('verify').disabled = true;
try {
if (!connected) {
const choice=walletChoices(getWallets(),'signMessage').find(w=>w.id===el('wallet').value);
el('status').textContent='Connect your everyday wallet and sign the sign-in message.';
const p=await openWallet(choice,'signMessage'), delegate=p.address;
const { id, message } = await post('challenge', { delegate });
const signed = await p.signMessage(new TextEncoder().encode(message), 'utf8');
if (p.address !== delegate || generation !== attempt) throw Error('Wallet changed. Please reconnect.');
const signature = btoa(String.fromCharCode(...(signed.signature ?? signed)));
await post('sign-in', { id, signature });
if (p.address !== delegate || generation !== attempt) throw Error('Wallet changed. Please reconnect.');
wallet = p; connected = delegate;
unsubscribe=p.watch(()=>void signOut());
el('leave').hidden = false; el('wallet').disabled = true; grantLink();
}
if (wallet.address !== connected) { await signOut(); throw Error('Wallet changed. Please reconnect.'); }
el('status').textContent = 'Checking delegated access on devnet…';
const vault = el('vault').value.trim(), mint = el('mint').value.trim();
const manual = el('manual').open;
const result = await post('holder', manual ? { vault, mint } : {});
if (attempt !== generation || vault !== el('vault').value.trim() || mint !== el('mint').value.trim()) return;
el('content').textContent = result.content;
el('holder').hidden = false;
el('status').textContent = 'Verified with your everyday wallet.';
el('grant').hidden = true;
} catch (e) {
if (attempt !== generation) return;
if (e.status === 401 && connected) await signOut();
if (e.status === 403 && connected) { el('manual').open = true; grantLink(); }
el('status').textContent = e.message || 'Verification was interrupted. Please try again.';
} finally {
el('verify').disabled = false;
}
});
try {
const registry=getWallets();
const refresh=()=>{
if (connected) return;
const select=el('wallet'), previous=select.value;
const choices=walletChoices(registry,'signMessage');
select.replaceChildren(...choices.map(choice=>new Option(choice.name,choice.id)));
select.value=choices.find(c=>c.id===previous && c.available)?.id ?? choices.find(c=>c.available)?.id ?? choices[0]?.id ?? '';
};
refresh();registry.on('register',refresh);registry.on('unregister',refresh);
const response = await fetch(base + 'settings');
if (!response.ok) throw Error();
settings = await response.json();
el('candidate').hidden = settings.rule !== 'collection';
el('verify').disabled = false;
el('site').textContent = new URL(settings.origin).host;
const labels = {wallet:'An active wallet-wide delegation',collection:'Verified collection',mint:'Eligible NFT',balance:'Required token balance · '+settings.minimum+' base units'};
el('status').textContent = labels[settings.rule] + (settings.address ? ' · '+settings.address : '');
} catch { el('status').textContent = 'The website owner needs to configure the pilot.'; }
tools/partner-verifier.mjs
// Every app supplies its own sign-in origin and eligibility policy.
import { randomBytes, createPublicKey, verify } from 'node:crypto';
import { PublicKey, SYSVAR_CLOCK_PUBKEY } from '@solana/web3.js';
import { getMetadataAccountDataSerializer, Key, TokenStandard } from '@metaplex-foundation/mpl-token-metadata';
import { ALL_APPS, siteOrigin } from '../lib/partners.mjs';
import {
AUDIENCE, GRANT_SIZE, audienceBytes, checkDeployment, grantPda, grantStatus, isAddress,
readClock, readGrant, readState, vaultPda,
} from '../lib/registry.mjs';
const tokenProgram = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const ed25519PublicKeyPrefix = Buffer.from('302a300506032b6570032100','hex');
// Implement save, get, and atomic consume in your own session-bound storage.
// A storage adapter must bind the challenge to the requesting browser's session.
export async function issueChallenge({delegate,origin=AUDIENCE,signInOrigin=origin,sessionId,store}){
if(!sessionId || !isAddress(delegate) || !PublicKey.isOnCurve(new PublicKey(delegate).toBytes())) throw new Error('Invalid session or delegate.');
if (siteOrigin(origin) !== origin) throw Error('Use the configured HTTPS sign-in origin.');
if (signInOrigin !== origin && !/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(signInOrigin)) {
throw new Error('The sign-in website must match the partner.');
}
const id=randomBytes(24).toString('hex');
const issued=new Date(),expires=new Date(issued.getTime()+3*60_000);
const message=[
new URL(signInOrigin).host+' wants you to sign in with your Solana account:',delegate,'',
'Verify holder access for '+origin+' on Solana devnet.','','URI: '+signInOrigin,'Version: 1','Chain ID: solana:devnet',
'Nonce: '+id,'Issued At: '+issued.toISOString(),'Expiration Time: '+expires.toISOString()
].join('\n');
await store.save(id,{sessionId,origin,signInOrigin,delegate,message,expiresAt:expires.getTime()});
return {id,message,expiresAt:expires.toISOString()};
}
export async function authenticateChallenge({id,signature,origin=AUDIENCE,signInOrigin=origin,sessionId,store}){
const record=await store.get(id);
if(!record || !sessionId || record.origin!==origin || record.signInOrigin!==signInOrigin || record.sessionId!==sessionId || record.expiresAt<=Date.now()){
throw new Error('Sign-in challenge expired or unavailable.');
}
const key=createPublicKey({key:Buffer.concat([ed25519PublicKeyPrefix,new PublicKey(record.delegate).toBuffer()]),format:'der',type:'spki'});
const sig=Buffer.from(signature,'base64');
if(sig.length!==64 || !verify(null,Buffer.from(record.message,'utf8'),key,sig)) throw new Error('Invalid wallet signature.');
if(!(await store.consume(id,sessionId,record.expiresAt))) throw new Error('Challenge was already consumed.');
return record.delegate;
}
function holdsToken(account,vault,mint){
if(!account || account.executable || !account.owner.equals(tokenProgram)) return false;
const d=Buffer.from(account.data);
return d.length===165 && d.subarray(0,32).equals(mint.toBuffer()) && d.subarray(32,64).equals(vault.toBuffer())
&& d.readBigUInt64LE(64)===1n && (d[108]===1 || d[108]===2);
}
function isSingleMint(account){
if(!account || account.executable || !account.owner.equals(tokenProgram)) return false;
const d=Buffer.from(account.data);
return d.length===82 && d.readBigUInt64LE(36)===1n && d[44]===0 && d[45]===1;
}
const metadataProgram = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
const nftStandards = new Set([TokenStandard.NonFungible, TokenStandard.NonFungibleEdition,
TokenStandard.ProgrammableNonFungible, TokenStandard.ProgrammableNonFungibleEdition]);
function verifiedMember(info, mint, collection) {
if (!info || info.executable || !info.owner.equals(metadataProgram) || info.data[0] !== Key.MetadataV1) return false;
try {
const [data] = getMetadataAccountDataSerializer().deserialize(info.data);
return data.mint === mint.toBase58() && data.collection.__option === 'Some'
&& data.collection.value.verified && data.collection.value.key === collection
&& data.tokenStandard.__option === 'Some' && nftStandards.has(data.tokenStandard.value);
} catch {
return false;
}
}
// The signed-in identity belongs to the delegate. The verified vault is returned separately.
async function acceptedAudiences(origin,scope='wallet') {
if (siteOrigin(origin) !== origin) throw Error('Use the configured HTTPS sign-in origin.');
return Promise.all([ALL_APPS,origin].map(value => audienceBytes(value,scope)));
}
async function readDelegation(client, {authenticatedDelegate,vault,asset,origin=AUDIENCE}, extraKeys=[], minContextSlot, collection) {
const {connection,program}=client;
const candidates=[];
for (const audience of await acceptedAudiences(origin)) {
if (asset) candidates.push(grantPda(program,vault,authenticatedDelegate,audience,'asset',asset));
candidates.push(grantPda(program,vault,authenticatedDelegate,audience,'wallet',null));
}
const standardCount=candidates.length;
if (collection) for(const audience of await acceptedAudiences(origin,'collection')) {
candidates.push(grantPda(program,vault,authenticatedDelegate,audience,'collection',collection));
}
const keys=[vaultPda(program,vault),SYSVAR_CLOCK_PUBKEY,...candidates,...extraKeys];
const snapshot=await connection.getMultipleAccountsInfoAndContext(keys,{
commitment:'finalized', ...(minContextSlot === undefined ? {} : {minContextSlot}),
});
const [state,clock]=snapshot.value;
const time=readClock(clock);
if (Math.abs(Date.now()/1000-time)>90) throw Error('Devnet state is too stale to verify.');
const slot=snapshot.context.slot;
let result={eligible:false,slot,reason:'No active delegation matches this wallet and scope.'};
if (state) {
const epoch=readState(program,vault,state);
for (const [i,id] of candidates.entries()) {
const account=snapshot.value[2+i];
if (!account) continue;
const grant=readGrant(program,id,account);
if (grantStatus(grant,epoch,time)==='active') {
result={eligible:true,slot,vault,delegate:authenticatedDelegate,scope:i>=standardCount?'collection':grant.scope,asset:grant.asset,grant:id.toBase58(),expiresAt:grant.expiresAt};
break;
}
}
}
return {result,extra:snapshot.value.slice(2+candidates.length)};
}
// A registry relationship only. Apps that promise ownership or a balance must also verify it.
export async function checkDelegation(client, request) {
await checkDeployment(client);
return (await readDelegation(client,request)).result;
}
export function validateRule(rule, address, minimum) {
if (rule==='wallet') return;
if (!['mint','collection','balance'].includes(rule) || !isAddress(address)) throw Error('Choose a valid Solana mint or collection.');
if (rule==='balance' && (typeof minimum !== 'string' || !/^[1-9]\d{0,19}$/.test(minimum) || BigInt(minimum)>0xffffffffffffffffn)) {
throw Error('Enter a positive minimum in whole base units within the SPL token range.');
}
}
export async function checkHolderEligibility(client, request){
await checkDeployment(client);
return checkCandidate(client, request);
}
export async function findHolderEligibility(client, {authenticatedDelegate,origin=AUDIENCE,rule='wallet',address,minimum}) {
validateRule(rule,address,minimum);
await checkDeployment(client);
const audiences=await acceptedAudiences(origin);
if (rule==='mint' || rule==='collection') audiences.push(...await acceptedAudiences(origin,'collection'));
const groups=await Promise.all(audiences.map(hash => client.connection.getProgramAccounts(client.program,{
commitment:'finalized',filters:[{dataSize:GRANT_SIZE},{memcmp:{offset:33,bytes:authenticatedDelegate}},
{memcmp:{offset:65,bytes:new PublicKey(hash).toBase58()}}],
})));
const candidates=new Map(), broad=new Set();
const add=(vault,mint)=>candidates.set(vault+':'+(mint||''),{vault,qualifyingMint:mint});
for (const [index,rows] of groups.entries()) {
for (const row of rows.slice(0,40)) {
const grant=readGrant(client.program,row.pubkey,row.account);
if(grant.delegate!==authenticatedDelegate || !grant.audienceHash.equals(audiences[index]) || grant.expiry<=Date.now()/1000) continue;
if(index>=2) {
if(grant.scope!=='asset') continue;
if(rule==='mint') add(grant.owner,address);
else if(grant.asset===address) broad.add(grant.owner);
} else if(rule==='wallet') {
if(grant.scope==='wallet') add(grant.owner,undefined);
} else if(rule==='collection') {
if(grant.scope==='asset') add(grant.owner,grant.asset);
else broad.add(grant.owner);
} else if(grant.scope==='wallet' || grant.asset===address) add(grant.owner,address);
}
}
for (const vault of [...broad].slice(0,2)) {
if(candidates.size>=6) break;
const owner=new PublicKey(vault);
const tokens=await client.connection.getTokenAccountsByOwner(owner,{programId:tokenProgram},{commitment:'finalized'});
for(const {account} of tokens.value) {
if(account.data.length!==165) continue;
const mint=new PublicKey(account.data.subarray(0,32));
if(holdsToken(account,owner,mint)) add(vault,mint.toBase58());
if(candidates.size>=6) break;
}
}
for(const candidate of [...candidates.values()].slice(0,6)) {
const result=await checkCandidate(client,{...candidate,authenticatedDelegate,origin,rule,minimum,collection:rule==='collection'?address:undefined});
if(result.eligible) return result;
}
return {eligible:false,reason:'No eligible delegation found automatically. Check a specific vault below or create a delegation.'};
}
function tokenBalance(info,vault,mint) {
if (!info || info.executable || !info.owner.equals(tokenProgram)) return 0n;
const d=Buffer.from(info.data);
if (d.length!==165 || !d.subarray(0,32).equals(mint.toBuffer()) || !d.subarray(32,64).equals(vault.toBuffer()) || ![1,2].includes(d[108])) return 0n;
return d.readBigUInt64LE(64);
}
async function checkCandidate(client, request){
const {authenticatedDelegate,vault,qualifyingMint,collection,minimum,origin=AUDIENCE}=request;
const rule=request.rule ?? (collection===undefined?'mint':'collection');
validateRule(rule,rule==='collection'?collection:qualifyingMint,minimum);
const identity={authenticatedDelegate,vault,origin};
if (rule==='wallet') return (await readDelegation(client,identity)).result;
const owner=new PublicKey(vault),mint=new PublicKey(qualifyingMint);
const accounts=await client.connection.getTokenAccountsByOwner(owner,{mint},{commitment:'finalized'});
// Final authority comes from the common snapshot below. Deduplicate before summing balances.
const balances=new Map();
for(const {account,pubkey} of accounts.value) {
const amount=tokenBalance(account,owner,mint);
if(rule==='balance'?amount>0n:amount===1n) balances.set(pubkey.toBase58(),amount);
}
const tokens=[...balances].sort((a,b)=>a[1]===b[1]?0:a[1]>b[1]?-1:1).slice(0,rule==='balance'?90:1).map(([key])=>new PublicKey(key));
if (!tokens.length) return {eligible:false,reason:'The vault has no qualifying token balance.'};
const extraKeys=[mint,...tokens];
let expectedCollection=rule==='collection'?collection:undefined;
let slot=accounts.context.slot;
if(rule==='mint' || rule==='collection') {
const metadataKey=PublicKey.findProgramAddressSync([Buffer.from('metadata'),metadataProgram.toBuffer(),mint.toBuffer()],metadataProgram)[0];
if(rule==='mint') {
// Discover a possible collection. Membership is authorized only in the final snapshot.
const hint=await client.connection.getAccountInfoAndContext(metadataKey,{commitment:'finalized',minContextSlot:slot});
slot=hint.context.slot;
if(hint.value?.owner.equals(metadataProgram)) {
try {
const [record]=getMetadataAccountDataSerializer().deserialize(hint.value.data);
if(record.collection.__option==='Some' && record.collection.value.verified) expectedCollection=record.collection.value.key;
} catch {}
}
}
if(expectedCollection) extraKeys.push(metadataKey);
}
const {result,extra}=await readDelegation(client,{...identity,asset:qualifyingMint},extraKeys,slot,expectedCollection);
if (!result.eligible) return result;
const [mintInfo,...others]=extra;
if(rule==='balance') {
const validMint=mintInfo && !mintInfo.executable && mintInfo.owner.equals(tokenProgram) && mintInfo.data.length===82 && mintInfo.data[45]===1;
const total=others.reduce((sum,info)=>sum+tokenBalance(info,owner,mint),0n);
if (!validMint || total<BigInt(minimum)) return {eligible:false,reason:'The vault does not meet the required token balance.'};
} else {
if(!isSingleMint(mintInfo) || !holdsToken(others[0],owner,mint)) return {eligible:false,reason:'NFT ownership could not be verified.'};
if((rule==='collection' || result.scope==='collection') && !verifiedMember(others[1],mint,expectedCollection)) return {eligible:false,reason:'This NFT is outside the configured verified collection.'};
}
return result;
}
lib/registry.mjs
import { Buffer } from 'buffer';
import {
ComputeBudgetProgram, Connection, PublicKey, SystemProgram, SYSVAR_CLOCK_PUBKEY,
Transaction, TransactionInstruction, VersionedTransaction,
} from '@solana/web3.js';
import { ALL_APPS, AUDIENCE, LEGACY_MAIL, grantAudience } from './partners.mjs';
export { AUDIENCE };
export const DEVNET_RPC = 'https://api.devnet.solana.com';
export const DEVNET_PROGRAM_ID = 'FX2zKgkTSrmUpQcfNsrahQN7Mer5gfPo6Ec8FLX3SuaR';
export const DEVNET_GENESIS = 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG';
const registryDigest = '9fdcd1f09c0910a6318eb8f5fd9f963a622d4453987d8c815da9466cc17163f1';
export const GRANT_SIZE = 146;
export const STATE_SIZE = 41;
export const YEAR = 366 * 86400;
const upgradeableLoader = new PublicKey('BPFLoaderUpgradeab1e11111111111111111111111');
const zero = Buffer.alloc(32);
const ascii = s => Buffer.from(s, 'utf8');
export function isAddress(value) {
if (typeof value !== 'string' || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value)) return false;
try { return new PublicKey(value).toBase58() === value; } catch { return false; }
}
export function normalizedGrant(owner, draft, nowSeconds) {
const delegate = String(draft.delegate ?? '').trim();
const scope = draft.scope;
const asset = scope === 'wallet' ? null : String(draft.asset ?? '').trim();
if (!isAddress(owner) || !isAddress(delegate) || !PublicKey.isOnCurve(new PublicKey(delegate).toBytes()) || delegate === SystemProgram.programId.toBase58()) {
throw new Error('Enter a valid signing wallet address for the delegate.');
}
if (owner === delegate) throw new Error('Choose a different wallet as your delegate.');
if (!['wallet','asset','collection'].includes(scope)) throw new Error('Choose a wallet, collection or asset.');
if (scope !== 'wallet' && (!isAddress(asset) || asset === SystemProgram.programId.toBase58())) {
throw new Error('Enter the full Solana asset address.');
}
const end = Math.floor(new Date(draft.expiresAt).getTime() / 1000);
if (!Number.isSafeInteger(end) || end <= nowSeconds || end > nowSeconds + YEAR) {
throw new Error('Choose a future expiry within 366 days.');
}
return {owner,delegate,scope,asset,expiresAt:new Date(end * 1000).toISOString(), audience:grantAudience(draft.audience)};
}
export async function audienceBytes(origin = ALL_APPS, scope = 'wallet') {
if (grantAudience(origin) !== origin) throw new Error('Audience must be a canonical HTTPS origin or the all-apps permission.');
const domain = scope === 'collection' ? 'urn:solmate:verified-collection:v1:' + origin : origin;
return Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', ascii(domain)));
}
export function vaultPda(program, vault) {
return PublicKey.findProgramAddressSync([ascii('vault'),new PublicKey(vault).toBuffer()],program)[0];
}
export function grantPda(program, vault, delegate, audience, scope, asset) {
if (!['wallet','asset','collection'].includes(scope)) throw new Error('Unsupported asset scope.');
const assetBytes = scope === 'wallet' ? zero : new PublicKey(asset).toBuffer();
return PublicKey.findProgramAddressSync([
ascii('grant'),new PublicKey(vault).toBuffer(),new PublicKey(delegate).toBuffer(),
audience,Buffer.from([scope === 'wallet' ? 0 : 1]),assetBytes
],program)[0];
}
const meta = (pubkey,isSigner,isWritable) => ({pubkey:new PublicKey(pubkey),isSigner,isWritable});
export function createGrantIx(program, grant, audience) {
const data = Buffer.alloc(106);
data[0] = 0; data[1] = grant.scope === 'wallet' ? 0 : 1;
new PublicKey(grant.delegate).toBuffer().copy(data,2);
Buffer.from(audience).copy(data,34);
if (grant.scope !== 'wallet') new PublicKey(grant.asset).toBuffer().copy(data,66);
data.writeBigInt64LE(BigInt(Date.parse(grant.expiresAt)/1000),98);
const state = vaultPda(program,grant.owner);
const id = grantPda(program,grant.owner,grant.delegate,audience,grant.scope,grant.asset);
return new TransactionInstruction({programId:program,keys:[
meta(grant.owner,true,true),meta(state,false,true),meta(id,false,true),meta(SystemProgram.programId,false,false)
],data});
}
export function revokeIx(program, owner, grantId) {
return new TransactionInstruction({programId:program,keys:[meta(owner,true,true),meta(grantId,false,true)],data:Buffer.from([1])});
}
export function revokeAllIx(program, owner) {
return new TransactionInstruction({programId:program,keys:[meta(owner,true,false),meta(vaultPda(program,owner),false,true)],data:Buffer.from([2])});
}
function recordBytes(info, program, tag, size) {
if (!info || !info.owner.equals(program) || info.executable) throw new Error('Invalid or missing registry record.');
const bytes = Buffer.from(info.data);
if (bytes.length !== size || bytes[0] !== tag) throw new Error('Unsupported registry record.');
return bytes;
}
const addressAt = (bytes,offset) => new PublicKey(bytes.subarray(offset,offset+32)).toBase58();
export function readState(program, vault, info) {
const bytes = recordBytes(info,program,1,STATE_SIZE);
if(addressAt(bytes,1)!==vault) throw new Error('Vault state belongs to a different address.');
return bytes.readBigUInt64LE(33);
}
export function readGrant(program, address, info) {
const data = recordBytes(info,program,2,GRANT_SIZE);
const owner = addressAt(data,1), delegate = addressAt(data,33);
const audienceHash = data.subarray(65,97);
const scope = data[97] === 0 ? 'wallet' : data[97] === 1 ? 'asset' : null;
if (!scope || (scope === 'wallet' && !data.subarray(98,130).equals(zero))) throw new Error('Malformed grant scope.');
const asset = scope === 'asset' ? addressAt(data,98) : null;
const id = grantPda(program,owner,delegate,audienceHash,scope,asset).toBase58();
if (id !== new PublicKey(address).toBase58()) throw new Error('Non-canonical registry record.');
const epoch = data.readBigUInt64LE(130), expiry = Number(data.readBigInt64LE(138));
if (!Number.isSafeInteger(expiry) || !Number.isFinite(new Date(expiry*1000).getTime())) throw new Error('Invalid grant expiry.');
return {owner,delegate,scope,asset,id,audienceHash,epoch,expiry,expiresAt:new Date(expiry*1000).toISOString()};
}
export function readClock(info) {
const data = info && Buffer.from(info.data);
if (!data || data.length < 40 || info.owner.toBase58() !== 'Sysvar1111111111111111111111111111111111111') throw new Error('Could not verify the Solana clock.');
const seconds = Number(data.readBigInt64LE(32));
if (!Number.isSafeInteger(seconds) || seconds < 1) throw new Error('Invalid Solana clock.');
return seconds;
}
export function grantStatus(record,epoch,time) {
if (epoch !== record.epoch) return 'revoked';
return record.expiry <= time ? 'expired' : 'active';
}
export function openDevnet(programAddress,endpoint = DEVNET_RPC) {
if (!programAddress) throw new Error('The devnet registry is awaiting deployment. Use a sample wallet to explore.');
if (!isAddress(programAddress) || programAddress === SystemProgram.programId.toBase58()) throw new Error('Devnet registry configuration is invalid.');
if (new URL(endpoint).protocol !== 'https:') throw new Error('Use an HTTPS devnet RPC.');
return {connection:new Connection(endpoint,'finalized'),program:new PublicKey(programAddress)};
}
export async function checkDeployment({connection,program}) {
if (await connection.getGenesisHash() !== DEVNET_GENESIS) throw new Error('Wallet actions are restricted to Solana devnet.');
const info = await connection.getAccountInfo(program,'finalized');
if (!info?.executable || !info.owner.equals(upgradeableLoader) || info.data.length !== 36 || Buffer.from(info.data).readUInt32LE(0) !== 2) {
throw new Error('The configured devnet registry is unavailable.');
}
const code = new PublicKey(info.data.subarray(4));
const canonicalCode = PublicKey.findProgramAddressSync([program.toBuffer()],upgradeableLoader)[0];
if (!code.equals(canonicalCode)) throw new Error('The registry has an invalid deployment address.');
const stored = await connection.getAccountInfo(code,'finalized');
if (!stored || stored.executable || !stored.owner.equals(upgradeableLoader) || stored.data.length <= 45 || Buffer.from(stored.data).readUInt32LE(0) !== 3) {
throw new Error('The configured devnet program has no live program data.');
}
const digest = await globalThis.crypto.subtle.digest('SHA-256', stored.data.subarray(45));
if (Buffer.from(digest).toString('hex') !== registryDigest) {
throw new Error('The deployed devnet code has changed. A verified Solmate release is required.');
}
}
export async function listChainGrants(client, address, direction='outgoing', knownOrigins=[]) {
const {connection,program} = client;
await checkDeployment(client);
const found = await connection.getProgramAccounts(program,{commitment:'finalized',filters:[
{dataSize:GRANT_SIZE},{memcmp:{offset:direction==='incoming'?33:1,bytes:address}}
],dataSlice:{offset:0,length:0},withContext:true});
const rows = [];
const names = new Map();
for (const origin of new Set([ALL_APPS,AUDIENCE,LEGACY_MAIL,...knownOrigins])) {
for (const kind of ['wallet','collection']) names.set((await audienceBytes(origin,kind)).toString('hex'),{origin,kind});
}
// Discover addresses first. Grant, revocation counter and clock below are read at a common slot.
for (let i=0;i<found.value.length;i+=45) {
const keys = found.value.slice(i,i+45).map(v=>v.pubkey);
const discovery = await connection.getMultipleAccountsInfoAndContext(keys,{commitment:'finalized',minContextSlot:found.context.slot});
const live = keys.flatMap((key,j)=>discovery.value[j]?[{key,owner:readGrant(program,key,discovery.value[j]).owner}]:[]);
if (!live.length) continue;
const states = live.map(v=>vaultPda(program,v.owner));
const bundle = await connection.getMultipleAccountsInfoAndContext([...live.map(v=>v.key),...states,SYSVAR_CLOCK_PUBKEY],{commitment:'finalized',minContextSlot:discovery.context.slot});
const time = readClock(bundle.value.at(-1));
live.forEach(({key,owner},j)=>{
if(!bundle.value[j]) return;
const grant = readGrant(program,key,bundle.value[j]);
if ((direction==='incoming'?grant.delegate:grant.owner)!==address) return;
const status = grantStatus(grant,readState(program,owner,bundle.value[live.length+j]),time);
const hash=grant.audienceHash.toString('hex'), label=names.get(hash);
rows.push({id:grant.id,owner,delegate:grant.delegate,scope:label?.kind==='collection'?'collection':grant.scope,asset:grant.asset,expiresAt:grant.expiresAt,status,
audience:label?.origin ?? 'SHA-256 '+hash,mode:'devnet'});
});
}
return rows.sort((a,b)=>b.expiresAt.localeCompare(a.expiresAt));
}
export async function buildTransaction(client,owner,ix) {
const {connection} = client;
const latest = await connection.getLatestBlockhash('confirmed');
// Specify the budget before review so wallets can sign without adding their own fee.
// At 1 micro-lamport per unit, the 200,000-unit limit costs one lamport after rounding.
const tx = new Transaction({feePayer:new PublicKey(owner),...latest}).add(
ComputeBudgetProgram.setComputeUnitLimit({units:200_000}),
ComputeBudgetProgram.setComputeUnitPrice({microLamports:1}),
ix,
);
const msg = tx.compileMessage();
const fee = await connection.getFeeForMessage(msg,'confirmed');
if (!Number.isSafeInteger(fee.value) || fee.value < 0) throw new Error('Fee unavailable. Please review again.');
const sim = await connection.simulateTransaction(new VersionedTransaction(msg),{commitment:'confirmed',sigVerify:false});
if (sim.value.err) throw new Error('Devnet simulation failed. Check the wallet balance, grant details and expiry.');
return {tx,latest,feeLamports:fee.value,bytes:Buffer.from(tx.serializeMessage())};
}
export async function sendPrepared(client,prepared,sign) {
const {connection} = client;
if (await connection.getBlockHeight('confirmed') > prepared.latest.lastValidBlockHeight) throw new Error('This review expired. Review the transaction again.');
const returned = await sign(prepared.tx);
// Verify locally, including for injected wallets that return their own Transaction object.
let signed;
try {
signed = Transaction.from(returned.serialize({requireAllSignatures:false,verifySignatures:false}));
} catch {
throw new Error('Solmate could not read the signed transaction. Reconnect your vault wallet and review again.');
}
if (!Buffer.from(signed.serializeMessage()).equals(prepared.bytes)) {
throw new Error('The wallet changed the reviewed transaction. Solmate stopped before sending. Review again using the displayed network fee.');
}
if (!signed.verifySignatures()) {
throw new Error('The vault signature could not be verified. Select the same vault account in your wallet and review again.');
}
const wire = signed.serialize();
const signature = await connection.sendRawTransaction(wire,{skipPreflight:false,preflightCommitment:'confirmed',maxRetries:3});
try {
const result = await connection.confirmTransaction({...prepared.latest,signature},'finalized');
if(result.value.err) throw new Error('Failed at finalization.');
} catch (cause) {
const error = new Error('Check this devnet transaction before retrying: '+signature,{cause});
error.signature=signature; throw error;
}
return {signature,finalized:true};
}
lib/partners.mjs
export const AUDIENCE = 'https://solmate.io';
// Kept separate from every HTTPS origin so a previous grant can never become universal.
export const ALL_APPS = 'urn:solmate:delegation:all-apps:v1';
export const LEGACY_MAIL = 'https://wagmimail.io';
export function siteOrigin(value) {
let url;
try { url = new URL(String(value).trim()); } catch {}
if (!url || url.protocol !== 'https:' || url.username || url.password || url.pathname !== '/' || url.href !== url.origin + '/') {
throw new Error('Enter an HTTPS website origin such as https://your-app.example, without a page path.');
}
return url.origin;
}
export function grantAudience(origin = ALL_APPS) {
return origin === ALL_APPS ? ALL_APPS : siteOrigin(origin);
}
export function audienceLabel(origin) {
return origin === ALL_APPS ? 'All integrated apps' : origin;
}
export function appPath(value = '/access') {
if (value === '/') return value;
if (typeof value !== 'string' || !/^\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/.test(value)) {
throw new Error('Use a page path such as /access or /members/enter.');
}
return value;
}
lib/browser-wallets.mjs
// Both the studio and the example dapp use the same wallet selection rules.
const ids = new WeakMap();
let nextId = 0;
const chain = 'solana:devnet';
export function walletChoices(registry, capability, host = globalThis.window) {
if (!host) return [];
const choices = [];
const feature = 'solana:' + capability;
for (const wallet of registry.get()) {
if (!wallet.chains.includes(chain) || !wallet.features['standard:connect'] || !wallet.features[feature]) continue;
if (capability === 'signTransaction' && !wallet.features[feature].supportedTransactionVersions?.includes('legacy')) continue;
if (!ids.has(wallet)) ids.set(wallet, 'standard-' + ++nextId);
choices.push({id:ids.get(wallet),name:wallet.name,available:true,standard:wallet});
}
for (const [id,name,provider] of [
['phantom','Phantom',host.phantom?.solana ?? (host.solana?.isPhantom ? host.solana : null)],
['solflare','Solflare',host.solflare?.isSolflare ? host.solflare : null],
]) {
if (!choices.some(choice=>choice.name.toLowerCase()===id)) choices.push({id,name,provider,available:typeof provider?.[capability]==='function'});
}
return choices;
}
export async function openWallet(choice, capability) {
if (!choice?.available) throw Error('Enable your wallet extension or open this page in its wallet browser.');
if (!choice.standard) {
const p=choice.provider;
await p.connect();
return {
get address(){return p.publicKey?.toBase58();},
disconnect:()=>p.disconnect?.(),
signMessage:async bytes=>{const signed=await p.signMessage(bytes,'utf8');return signed.signature ?? signed;},
signTransaction:tx=>p.signTransaction(tx),
watch:fn=>{
p.on?.('accountChanged',fn);p.on?.('disconnect',fn);
return ()=>{p.removeListener?.('accountChanged',fn);p.removeListener?.('disconnect',fn);};
},
};
}
const w=choice.standard, feature='solana:'+capability;
const {accounts}=await w.features['standard:connect'].connect();
const selected=accounts.find(a=>a.chains.includes(chain) && a.features.includes(feature));
if (!selected) throw Error('Choose an account that supports this action on Solana devnet.');
const account=()=>{
const current=w.accounts.find(a=>a.address===selected.address && a.chains.includes(chain) && a.features.includes(feature));
if (!current) throw Error('Wallet account changed. Reconnect to continue.');
return current;
};
return {
get address(){return w.accounts.find(a=>a.address===selected.address)?.address;},
disconnect:()=>w.features['standard:disconnect']?.disconnect(),
watch:fn=>w.features['standard:events']?.on('change',event=>{if(event.accounts && !event.accounts.some(a=>a.address===selected.address)) fn();}) ?? (()=>{}),
signMessage:async bytes=>{
const [result]=await w.features['solana:signMessage'].signMessage({account:account(),message:bytes});
if (result.signedMessage.length!==bytes.length || !bytes.every((v,i)=>result.signedMessage[i]===v)) throw Error('The signed message changed.');
return result.signature;
},
signTransaction:async tx=>{
const [result]=await w.features['solana:signTransaction'].signTransaction({account:account(),chain,
transaction:tx.serialize({requireAllSignatures:false,verifySignatures:false})});
return tx.constructor.from(result.signedTransaction);
},
};
}