@stablecoin.xyz/radius-mpp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/{chunk-TM4JSIP6.js → chunk-HPDJWSJ3.js} +8 -2
- package/dist/chunk-HPDJWSJ3.js.map +1 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +3 -3
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +8 -4
- package/src/index.ts +6 -0
- package/src/server.ts +8 -2
- package/dist/chunk-TM4JSIP6.js.map +0 -1
package/README.md
CHANGED
|
@@ -84,7 +84,29 @@ const mppx = Mppx.create({
|
|
|
84
84
|
| `confirmations` | `1` | How many block confirmations to wait before accepting the payment. Radius has fast finality so `1` is usually sufficient. Increase for high-value transactions where you want extra certainty the tx won't reorg. |
|
|
85
85
|
| `rpcUrl` | Chain's default RPC | Override the RPC endpoint used to fetch and verify transaction receipts. Useful if you're running your own Radius node or need a higher-throughput RPC provider. |
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
All options are optional — `radius()` with no arguments works out of the box using chain defaults.
|
|
88
|
+
|
|
89
|
+
### Payer enforcement
|
|
90
|
+
|
|
91
|
+
By default, any wallet can pay for a gated endpoint. If you need to restrict payments to a specific wallet (e.g. the authenticated user's wallet), pass `payer` in the charge options:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
app.get(
|
|
95
|
+
'/premium',
|
|
96
|
+
mppx.charge({
|
|
97
|
+
amount: '1000000',
|
|
98
|
+
currency: 'SBC',
|
|
99
|
+
chainId: radiusMainnet.id,
|
|
100
|
+
token: SBC_TOKEN.mainnet,
|
|
101
|
+
recipient: MERCHANT_WALLET,
|
|
102
|
+
payer: userSession.walletAddress, // only accept payment from this wallet
|
|
103
|
+
expires: Expires.minutes(5),
|
|
104
|
+
}),
|
|
105
|
+
handler,
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
When `payer` is set, the server verifies that `receipt.from` matches the specified address. If someone else's wallet pays, the server rejects the payment. When `payer` is omitted, any wallet can pay.
|
|
88
110
|
|
|
89
111
|
See [`examples/`](./examples) for complete working examples with Hono, Express, Next.js, and Elysia.
|
|
90
112
|
|
|
@@ -82,7 +82,13 @@ var charge = Method.from({
|
|
|
82
82
|
/** ERC-20 token contract address on Radius */
|
|
83
83
|
token: z.string(),
|
|
84
84
|
/** Recipient address (the seller / payee) */
|
|
85
|
-
recipient: z.string()
|
|
85
|
+
recipient: z.string(),
|
|
86
|
+
/**
|
|
87
|
+
* Optional payer address to enforce sender identity.
|
|
88
|
+
* When set, the server rejects payments from any other wallet.
|
|
89
|
+
* Useful when the paying wallet must match an authenticated session.
|
|
90
|
+
*/
|
|
91
|
+
payer: z.optional(z.string())
|
|
86
92
|
})
|
|
87
93
|
}
|
|
88
94
|
});
|
|
@@ -97,4 +103,4 @@ export {
|
|
|
97
103
|
ERC20_ABI,
|
|
98
104
|
charge
|
|
99
105
|
};
|
|
100
|
-
//# sourceMappingURL=chunk-
|
|
106
|
+
//# sourceMappingURL=chunk-HPDJWSJ3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/constants.ts"],"sourcesContent":["/**\n * @stablecoin.xyz/radius-mpp\n *\n * Shared method definition for Radius ERC-20 payments via MPP.\n * Import from the root path in code that needs the schema but not\n * client or server logic (e.g. shared types).\n */\n\nimport { Method, z } from 'mppx'\n\nexport { radiusMainnet, radiusTestnet, SBC_TOKEN, SBC_DECIMALS, explorerTxUrl } from './constants.js'\n\n/**\n * Radius charge method — one-time ERC-20 token transfer on Radius.\n *\n * Request schema: the Challenge fields the server sends to the client.\n * Credential payload: what the client returns after broadcasting the tx.\n */\nexport const charge = Method.from({\n intent: 'charge',\n name: 'radius',\n schema: {\n credential: {\n payload: z.object({\n /** The on-chain transaction hash of the ERC-20 transfer */\n txHash: z.string(),\n }),\n },\n request: z.object({\n /** Transfer amount in atomic units (e.g. \"1000000\" for 1 SBC with 6 decimals) */\n amount: z.string(),\n /** Token symbol or identifier (e.g. \"SBC\") */\n currency: z.string(),\n /** Chain ID (723487 for mainnet, 72344 for testnet) */\n chainId: z.number(),\n /** ERC-20 token contract address on Radius */\n token: z.string(),\n /** Recipient address (the seller / payee) */\n recipient: z.string(),\n /**\n * Optional payer address to enforce sender identity.\n * When set, the server rejects payments from any other wallet.\n * Useful when the paying wallet must match an authenticated session.\n */\n payer: z.optional(z.string()),\n }),\n },\n})\n\n/** Re-export the inferred types for external consumers */\nexport type RadiusCharge = typeof charge\n","import { defineChain } from 'viem'\n\n// ---------------------------------------------------------------------------\n// Token addresses (SBC stablecoin on Radius)\n// ---------------------------------------------------------------------------\n\nexport const SBC_TOKEN = {\n mainnet: '0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb' as const,\n testnet: '0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb' as const,\n}\n\nexport const SBC_DECIMALS = 6\n\n// ---------------------------------------------------------------------------\n// Chain definitions (viem-compatible)\n// ---------------------------------------------------------------------------\n\nexport const radiusMainnet = defineChain({\n id: 723487,\n name: 'Radius',\n nativeCurrency: { name: 'RUSD', symbol: 'RUSD', decimals: 18 },\n rpcUrls: {\n default: { http: ['https://rpc.radiustech.xyz'] },\n },\n blockExplorers: {\n default: { name: 'Radius Explorer', url: 'https://network.radiustech.xyz' },\n },\n})\n\nexport const radiusTestnet = defineChain({\n id: 72344,\n name: 'Radius Testnet',\n nativeCurrency: { name: 'RUSD', symbol: 'RUSD', decimals: 18 },\n rpcUrls: {\n default: { http: ['https://rpc.testnet.radiustech.xyz'] },\n },\n blockExplorers: {\n default: { name: 'Radius Testnet Explorer', url: 'https://testnet.radiustech.xyz' },\n },\n testnet: true,\n})\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Resolve chain definition by chain ID. Returns mainnet for 723 (legacy alias). */\nexport function resolveChain(chainId: number) {\n if (chainId === radiusMainnet.id || chainId === 723) return radiusMainnet\n if (chainId === radiusTestnet.id) return radiusTestnet\n return null\n}\n\n/** Resolve the SBC token address for a given chain ID. */\nexport function resolveTokenAddress(chainId: number) {\n if (chainId === radiusMainnet.id || chainId === 723) return SBC_TOKEN.mainnet\n if (chainId === radiusTestnet.id) return SBC_TOKEN.testnet\n return null\n}\n\n/** Build an explorer URL for a transaction hash on a given chain. */\nexport function explorerTxUrl(chainId: number, txHash: string): string | null {\n const chain = resolveChain(chainId)\n if (!chain?.blockExplorers?.default) return null\n return `${chain.blockExplorers.default.url}/tx/${txHash}`\n}\n\n// Standard ERC-20 ABI subset used by client and server\nexport const ERC20_ABI = [\n {\n inputs: [\n { name: 'to', type: 'address' },\n { name: 'amount', type: 'uint256' },\n ],\n name: 'transfer',\n outputs: [{ name: '', type: 'bool' }],\n stateMutability: 'nonpayable',\n type: 'function',\n },\n {\n inputs: [{ name: 'account', type: 'address' }],\n name: 'balanceOf',\n outputs: [{ name: '', type: 'uint256' }],\n stateMutability: 'view',\n type: 'function',\n },\n] as const\n"],"mappings":";AAQA,SAAS,QAAQ,SAAS;;;ACR1B,SAAS,mBAAmB;AAMrB,IAAM,YAAY;AAAA,EACvB,SAAS;AAAA,EACT,SAAS;AACX;AAEO,IAAM,eAAe;AAMrB,IAAM,gBAAgB,YAAY;AAAA,EACvC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,EAAE,MAAM,QAAQ,QAAQ,QAAQ,UAAU,GAAG;AAAA,EAC7D,SAAS;AAAA,IACP,SAAS,EAAE,MAAM,CAAC,4BAA4B,EAAE;AAAA,EAClD;AAAA,EACA,gBAAgB;AAAA,IACd,SAAS,EAAE,MAAM,mBAAmB,KAAK,iCAAiC;AAAA,EAC5E;AACF,CAAC;AAEM,IAAM,gBAAgB,YAAY;AAAA,EACvC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,EAAE,MAAM,QAAQ,QAAQ,QAAQ,UAAU,GAAG;AAAA,EAC7D,SAAS;AAAA,IACP,SAAS,EAAE,MAAM,CAAC,oCAAoC,EAAE;AAAA,EAC1D;AAAA,EACA,gBAAgB;AAAA,IACd,SAAS,EAAE,MAAM,2BAA2B,KAAK,iCAAiC;AAAA,EACpF;AAAA,EACA,SAAS;AACX,CAAC;AAOM,SAAS,aAAa,SAAiB;AAC5C,MAAI,YAAY,cAAc,MAAM,YAAY,IAAK,QAAO;AAC5D,MAAI,YAAY,cAAc,GAAI,QAAO;AACzC,SAAO;AACT;AAUO,SAAS,cAAc,SAAiB,QAA+B;AAC5E,QAAM,QAAQ,aAAa,OAAO;AAClC,MAAI,CAAC,OAAO,gBAAgB,QAAS,QAAO;AAC5C,SAAO,GAAG,MAAM,eAAe,QAAQ,GAAG,OAAO,MAAM;AACzD;AAGO,IAAM,YAAY;AAAA,EACvB;AAAA,IACE,QAAQ;AAAA,MACN,EAAE,MAAM,MAAM,MAAM,UAAU;AAAA,MAC9B,EAAE,MAAM,UAAU,MAAM,UAAU;AAAA,IACpC;AAAA,IACA,MAAM;AAAA,IACN,SAAS,CAAC,EAAE,MAAM,IAAI,MAAM,OAAO,CAAC;AAAA,IACpC,iBAAiB;AAAA,IACjB,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,QAAQ,CAAC,EAAE,MAAM,WAAW,MAAM,UAAU,CAAC;AAAA,IAC7C,MAAM;AAAA,IACN,SAAS,CAAC,EAAE,MAAM,IAAI,MAAM,UAAU,CAAC;AAAA,IACvC,iBAAiB;AAAA,IACjB,MAAM;AAAA,EACR;AACF;;;ADpEO,IAAM,SAAS,OAAO,KAAK;AAAA,EAChC,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,IACN,YAAY;AAAA,MACV,SAAS,EAAE,OAAO;AAAA;AAAA,QAEhB,QAAQ,EAAE,OAAO;AAAA,MACnB,CAAC;AAAA,IACH;AAAA,IACA,SAAS,EAAE,OAAO;AAAA;AAAA,MAEhB,QAAQ,EAAE,OAAO;AAAA;AAAA,MAEjB,UAAU,EAAE,OAAO;AAAA;AAAA,MAEnB,SAAS,EAAE,OAAO;AAAA;AAAA,MAElB,OAAO,EAAE,OAAO;AAAA;AAAA,MAEhB,WAAW,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMpB,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC;AAAA,IAC9B,CAAC;AAAA,EACH;AACF,CAAC;","names":[]}
|
package/dist/client.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ interface RadiusClientConfig {
|
|
|
16
16
|
* Must have an account attached (e.g. via privateKeyToAccount or browser wallet).
|
|
17
17
|
*/
|
|
18
18
|
walletClient: WalletClient<Transport, Chain, Account>;
|
|
19
|
+
/**
|
|
20
|
+
* Number of block confirmations to wait before returning the credential.
|
|
21
|
+
* Defaults to 1 (Radius has fast finality).
|
|
22
|
+
*/
|
|
23
|
+
confirmations?: number;
|
|
19
24
|
}
|
|
20
25
|
declare function radius(config: RadiusClientConfig): Method.Client<RadiusCharge>;
|
|
21
26
|
|
package/dist/client.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ERC20_ABI,
|
|
3
3
|
charge,
|
|
4
4
|
resolveChain
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-HPDJWSJ3.js";
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
8
8
|
import { Credential, Method } from "mppx";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
encodeFunctionData
|
|
14
14
|
} from "viem";
|
|
15
15
|
function radius(config) {
|
|
16
|
-
const { walletClient } = config;
|
|
16
|
+
const { walletClient, confirmations = 1 } = config;
|
|
17
17
|
return Method.toClient(charge, {
|
|
18
18
|
async createCredential({ challenge }) {
|
|
19
19
|
const { amount, chainId, token, recipient } = challenge.request;
|
|
@@ -39,7 +39,7 @@ function radius(config) {
|
|
|
39
39
|
});
|
|
40
40
|
await publicClient.waitForTransactionReceipt({
|
|
41
41
|
hash: txHash,
|
|
42
|
-
confirmations
|
|
42
|
+
confirmations
|
|
43
43
|
});
|
|
44
44
|
return Credential.serialize({
|
|
45
45
|
challenge,
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * @stablecoin.xyz/radius-mpp/client\n *\n * Client-side: signs and broadcasts an ERC-20 transfer on Radius,\n * then returns the txHash as the Credential payload.\n */\n\nimport { Credential, Method } from 'mppx'\nimport { Mppx } from 'mppx/client'\nimport {\n createPublicClient,\n createWalletClient,\n http,\n type Account,\n type Chain,\n type Transport,\n type WalletClient,\n encodeFunctionData,\n} from 'viem'\nimport { charge, type RadiusCharge } from './index.js'\nimport { ERC20_ABI, resolveChain } from './constants.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface RadiusClientConfig {\n /**\n * A viem WalletClient that can sign and send transactions on Radius.\n * Must have an account attached (e.g. via privateKeyToAccount or browser wallet).\n */\n walletClient: WalletClient<Transport, Chain, Account>\n}\n\n// ---------------------------------------------------------------------------\n// Client method factory\n// ---------------------------------------------------------------------------\n\nexport function radius(config: RadiusClientConfig): Method.Client<RadiusCharge> {\n const { walletClient } = config\n\n return Method.toClient(charge, {\n async createCredential({ challenge }) {\n const { amount, chainId, token, recipient } = challenge.request\n\n // Resolve the Radius chain definition for RPC access\n const chain = resolveChain(chainId)\n if (!chain) {\n throw new Error(`Unsupported Radius chain ID: ${chainId}`)\n }\n\n // Build the ERC-20 transfer calldata\n const data = encodeFunctionData({\n abi: ERC20_ABI,\n functionName: 'transfer',\n args: [recipient as `0x${string}`, BigInt(amount)],\n })\n\n // Use the chain's RPC for gas estimation and receipt waiting\n const publicClient = createPublicClient({\n chain,\n transport: http(chain.rpcUrls.default.http[0]),\n })\n\n // Estimate gas price (Radius doesn't support EIP-1559 fee estimation)\n const gasPrice = await publicClient.getGasPrice()\n\n // Send the transaction using the resolved chain's RPC\n const txHash = await walletClient.sendTransaction({\n to: token as `0x${string}`,\n data,\n chain,\n gasPrice,\n })\n\n
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * @stablecoin.xyz/radius-mpp/client\n *\n * Client-side: signs and broadcasts an ERC-20 transfer on Radius,\n * then returns the txHash as the Credential payload.\n */\n\nimport { Credential, Method } from 'mppx'\nimport { Mppx } from 'mppx/client'\nimport {\n createPublicClient,\n createWalletClient,\n http,\n type Account,\n type Chain,\n type Transport,\n type WalletClient,\n encodeFunctionData,\n} from 'viem'\nimport { charge, type RadiusCharge } from './index.js'\nimport { ERC20_ABI, resolveChain } from './constants.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface RadiusClientConfig {\n /**\n * A viem WalletClient that can sign and send transactions on Radius.\n * Must have an account attached (e.g. via privateKeyToAccount or browser wallet).\n */\n walletClient: WalletClient<Transport, Chain, Account>\n\n /**\n * Number of block confirmations to wait before returning the credential.\n * Defaults to 1 (Radius has fast finality).\n */\n confirmations?: number\n}\n\n// ---------------------------------------------------------------------------\n// Client method factory\n// ---------------------------------------------------------------------------\n\nexport function radius(config: RadiusClientConfig): Method.Client<RadiusCharge> {\n const { walletClient, confirmations = 1 } = config\n\n return Method.toClient(charge, {\n async createCredential({ challenge }) {\n const { amount, chainId, token, recipient } = challenge.request\n\n // Resolve the Radius chain definition for RPC access\n const chain = resolveChain(chainId)\n if (!chain) {\n throw new Error(`Unsupported Radius chain ID: ${chainId}`)\n }\n\n // Build the ERC-20 transfer calldata\n const data = encodeFunctionData({\n abi: ERC20_ABI,\n functionName: 'transfer',\n args: [recipient as `0x${string}`, BigInt(amount)],\n })\n\n // Use the chain's RPC for gas estimation and receipt waiting\n const publicClient = createPublicClient({\n chain,\n transport: http(chain.rpcUrls.default.http[0]),\n })\n\n // Estimate gas price (Radius doesn't support EIP-1559 fee estimation)\n const gasPrice = await publicClient.getGasPrice()\n\n // Send the transaction using the resolved chain's RPC\n const txHash = await walletClient.sendTransaction({\n to: token as `0x${string}`,\n data,\n chain,\n gasPrice,\n })\n\n await publicClient.waitForTransactionReceipt({\n hash: txHash,\n confirmations,\n })\n\n return Credential.serialize({\n challenge,\n payload: { txHash },\n })\n },\n })\n}\n\n// Re-export Mppx so consumers only need one import\nexport { Mppx }\n"],"mappings":";;;;;;;AAOA,SAAS,YAAY,cAAc;AACnC,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EAEA;AAAA,EAKA;AAAA,OACK;AA0BA,SAAS,OAAO,QAAyD;AAC9E,QAAM,EAAE,cAAc,gBAAgB,EAAE,IAAI;AAE5C,SAAO,OAAO,SAAS,QAAQ;AAAA,IAC7B,MAAM,iBAAiB,EAAE,UAAU,GAAG;AACpC,YAAM,EAAE,QAAQ,SAAS,OAAO,UAAU,IAAI,UAAU;AAGxD,YAAM,QAAQ,aAAa,OAAO;AAClC,UAAI,CAAC,OAAO;AACV,cAAM,IAAI,MAAM,gCAAgC,OAAO,EAAE;AAAA,MAC3D;AAGA,YAAM,OAAO,mBAAmB;AAAA,QAC9B,KAAK;AAAA,QACL,cAAc;AAAA,QACd,MAAM,CAAC,WAA4B,OAAO,MAAM,CAAC;AAAA,MACnD,CAAC;AAGD,YAAM,eAAe,mBAAmB;AAAA,QACtC;AAAA,QACA,WAAW,KAAK,MAAM,QAAQ,QAAQ,KAAK,CAAC,CAAC;AAAA,MAC/C,CAAC;AAGD,YAAM,WAAW,MAAM,aAAa,YAAY;AAGhD,YAAM,SAAS,MAAM,aAAa,gBAAgB;AAAA,QAChD,IAAI;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,YAAM,aAAa,0BAA0B;AAAA,QAC3C,MAAM;AAAA,QACN;AAAA,MACF,CAAC;AAED,aAAO,WAAW,UAAU;AAAA,QAC1B;AAAA,QACA,SAAS,EAAE,OAAO;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -138,6 +138,12 @@ declare const charge: {
|
|
|
138
138
|
token: z.ZodMiniString<string>;
|
|
139
139
|
/** Recipient address (the seller / payee) */
|
|
140
140
|
recipient: z.ZodMiniString<string>;
|
|
141
|
+
/**
|
|
142
|
+
* Optional payer address to enforce sender identity.
|
|
143
|
+
* When set, the server rejects payments from any other wallet.
|
|
144
|
+
* Useful when the paying wallet must match an authenticated session.
|
|
145
|
+
*/
|
|
146
|
+
payer: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
141
147
|
}, z.core.$strip>;
|
|
142
148
|
};
|
|
143
149
|
};
|
package/dist/index.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
charge,
|
|
3
3
|
resolveChain
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-HPDJWSJ3.js";
|
|
5
5
|
|
|
6
6
|
// src/server.ts
|
|
7
7
|
import { Method, Receipt } from "mppx";
|
|
@@ -27,7 +27,7 @@ function radius(config = {}) {
|
|
|
27
27
|
return Method.toServer(charge, {
|
|
28
28
|
async verify({ credential }) {
|
|
29
29
|
const { txHash } = credential.payload;
|
|
30
|
-
const { amount, chainId, token, recipient } = credential.challenge.request;
|
|
30
|
+
const { amount, chainId, token, recipient, payer } = credential.challenge.request;
|
|
31
31
|
const chain = resolveChain(chainId);
|
|
32
32
|
if (!chain) {
|
|
33
33
|
throw new Error(`Unsupported Radius chain ID: ${chainId}`);
|
|
@@ -43,6 +43,11 @@ function radius(config = {}) {
|
|
|
43
43
|
if (receipt.status !== "success") {
|
|
44
44
|
throw new Error(`Transaction ${txHash} failed on-chain (status: ${receipt.status})`);
|
|
45
45
|
}
|
|
46
|
+
if (payer && receipt.from.toLowerCase() !== payer.toLowerCase()) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Transaction sender ${receipt.from} does not match expected payer ${payer}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
46
51
|
const txToAddress = receipt.to?.toLowerCase();
|
|
47
52
|
if (txToAddress !== token.toLowerCase()) {
|
|
48
53
|
throw new Error(
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * @stablecoin.xyz/radius-mpp/server\n *\n * Server-side: verifies that an ERC-20 transfer actually landed on Radius\n * with the correct amount, recipient, and token.\n */\n\nimport { Method, Receipt } from 'mppx'\nimport { Mppx, Expires, Store } from 'mppx/server'\nimport {\n createPublicClient,\n http,\n decodeEventLog,\n type Log,\n} from 'viem'\nimport { charge, type RadiusCharge } from './index.js'\nimport { resolveChain, SBC_DECIMALS } from './constants.js'\n\n// ---------------------------------------------------------------------------\n// ERC-20 Transfer event ABI (for log decoding)\n// ---------------------------------------------------------------------------\n\nconst TRANSFER_EVENT_ABI = [\n {\n type: 'event',\n name: 'Transfer',\n inputs: [\n { indexed: true, name: 'from', type: 'address' },\n { indexed: true, name: 'to', type: 'address' },\n { indexed: false, name: 'value', type: 'uint256' },\n ],\n },\n] as const\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface RadiusServerConfig {\n /**\n * Optional custom RPC URL. If not provided, uses the default from chain config.\n */\n rpcUrl?: string\n\n /**\n * Number of confirmations to require before accepting the tx.\n * Defaults to 1 (Radius has fast finality).\n */\n confirmations?: number\n}\n\n// ---------------------------------------------------------------------------\n// Server method factory\n// ---------------------------------------------------------------------------\n\nexport function radius(config: RadiusServerConfig = {}): Method.Server<RadiusCharge> {\n const { confirmations = 1 } = config\n\n return Method.toServer(charge, {\n async verify({ credential }) {\n const { txHash } = credential.payload\n const { amount, chainId, token, recipient } = credential.challenge.request\n\n // Resolve chain\n const chain = resolveChain(chainId)\n if (!chain) {\n throw new Error(`Unsupported Radius chain ID: ${chainId}`)\n }\n\n const rpcUrl = config.rpcUrl ?? chain.rpcUrls.default.http[0]\n const publicClient = createPublicClient({\n chain,\n transport: http(rpcUrl),\n })\n\n // Fetch the transaction receipt\n const receipt = await publicClient.getTransactionReceipt({\n hash: txHash as `0x${string}`,\n })\n\n // Check tx succeeded\n if (receipt.status !== 'success') {\n throw new Error(`Transaction ${txHash} failed on-chain (status: ${receipt.status})`)\n }\n\n //
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * @stablecoin.xyz/radius-mpp/server\n *\n * Server-side: verifies that an ERC-20 transfer actually landed on Radius\n * with the correct amount, recipient, and token.\n */\n\nimport { Method, Receipt } from 'mppx'\nimport { Mppx, Expires, Store } from 'mppx/server'\nimport {\n createPublicClient,\n http,\n decodeEventLog,\n type Log,\n} from 'viem'\nimport { charge, type RadiusCharge } from './index.js'\nimport { resolveChain, SBC_DECIMALS } from './constants.js'\n\n// ---------------------------------------------------------------------------\n// ERC-20 Transfer event ABI (for log decoding)\n// ---------------------------------------------------------------------------\n\nconst TRANSFER_EVENT_ABI = [\n {\n type: 'event',\n name: 'Transfer',\n inputs: [\n { indexed: true, name: 'from', type: 'address' },\n { indexed: true, name: 'to', type: 'address' },\n { indexed: false, name: 'value', type: 'uint256' },\n ],\n },\n] as const\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface RadiusServerConfig {\n /**\n * Optional custom RPC URL. If not provided, uses the default from chain config.\n */\n rpcUrl?: string\n\n /**\n * Number of confirmations to require before accepting the tx.\n * Defaults to 1 (Radius has fast finality).\n */\n confirmations?: number\n}\n\n// ---------------------------------------------------------------------------\n// Server method factory\n// ---------------------------------------------------------------------------\n\nexport function radius(config: RadiusServerConfig = {}): Method.Server<RadiusCharge> {\n const { confirmations = 1 } = config\n\n return Method.toServer(charge, {\n async verify({ credential }) {\n const { txHash } = credential.payload\n const { amount, chainId, token, recipient, payer } = credential.challenge.request\n\n // Resolve chain\n const chain = resolveChain(chainId)\n if (!chain) {\n throw new Error(`Unsupported Radius chain ID: ${chainId}`)\n }\n\n const rpcUrl = config.rpcUrl ?? chain.rpcUrls.default.http[0]\n const publicClient = createPublicClient({\n chain,\n transport: http(rpcUrl),\n })\n\n // Fetch the transaction receipt\n const receipt = await publicClient.getTransactionReceipt({\n hash: txHash as `0x${string}`,\n })\n\n // Check tx succeeded\n if (receipt.status !== 'success') {\n throw new Error(`Transaction ${txHash} failed on-chain (status: ${receipt.status})`)\n }\n\n // If payer is specified, verify the tx was sent by the expected wallet\n if (payer && receipt.from.toLowerCase() !== payer.toLowerCase()) {\n throw new Error(\n `Transaction sender ${receipt.from} does not match expected payer ${payer}`\n )\n }\n\n // Check the tx was sent to the correct token contract\n const txToAddress = receipt.to?.toLowerCase()\n if (txToAddress !== token.toLowerCase()) {\n throw new Error(\n `Transaction target ${txToAddress} does not match expected token ${token}`\n )\n }\n\n // Find the Transfer event log that matches recipient and amount\n const transferLog = findMatchingTransferLog(\n receipt.logs,\n token,\n recipient,\n BigInt(amount),\n )\n\n if (!transferLog) {\n throw new Error(\n `No matching ERC-20 Transfer event found for recipient ${recipient} with amount >= ${amount}`\n )\n }\n\n // Optionally wait for additional confirmations\n if (confirmations > 1) {\n await publicClient.waitForTransactionReceipt({\n hash: txHash as `0x${string}`,\n confirmations,\n })\n }\n\n return Receipt.from({\n method: 'radius',\n reference: txHash,\n status: 'success',\n timestamp: new Date().toISOString(),\n })\n },\n })\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction findMatchingTransferLog(\n logs: Log[],\n token: string,\n recipient: string,\n minAmount: bigint,\n) {\n for (const log of logs) {\n // Only look at logs from the correct token contract\n if (log.address.toLowerCase() !== token.toLowerCase()) continue\n\n try {\n const decoded = decodeEventLog({\n abi: TRANSFER_EVENT_ABI,\n data: log.data,\n topics: log.topics,\n })\n\n if (decoded.eventName !== 'Transfer') continue\n\n const { to, value } = decoded.args\n if (\n to.toLowerCase() === recipient.toLowerCase() &&\n value >= minAmount\n ) {\n return { from: decoded.args.from, to, value }\n }\n } catch {\n // Not a Transfer event from this ABI, skip\n continue\n }\n }\n\n return null\n}\n\n// Re-export Mppx, Expires, Store so consumers only need one import\nexport { Mppx, Expires, Store }\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -30,6 +30,12 @@ export interface RadiusClientConfig {
|
|
|
30
30
|
* Must have an account attached (e.g. via privateKeyToAccount or browser wallet).
|
|
31
31
|
*/
|
|
32
32
|
walletClient: WalletClient<Transport, Chain, Account>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Number of block confirmations to wait before returning the credential.
|
|
36
|
+
* Defaults to 1 (Radius has fast finality).
|
|
37
|
+
*/
|
|
38
|
+
confirmations?: number
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
// ---------------------------------------------------------------------------
|
|
@@ -37,7 +43,7 @@ export interface RadiusClientConfig {
|
|
|
37
43
|
// ---------------------------------------------------------------------------
|
|
38
44
|
|
|
39
45
|
export function radius(config: RadiusClientConfig): Method.Client<RadiusCharge> {
|
|
40
|
-
const { walletClient } = config
|
|
46
|
+
const { walletClient, confirmations = 1 } = config
|
|
41
47
|
|
|
42
48
|
return Method.toClient(charge, {
|
|
43
49
|
async createCredential({ challenge }) {
|
|
@@ -73,11 +79,9 @@ export function radius(config: RadiusClientConfig): Method.Client<RadiusCharge>
|
|
|
73
79
|
gasPrice,
|
|
74
80
|
})
|
|
75
81
|
|
|
76
|
-
// TODO: Make confirmation count configurable (Radius has fast blocks,
|
|
77
|
-
// 1 confirmation should suffice but we may want to allow tuning)
|
|
78
82
|
await publicClient.waitForTransactionReceipt({
|
|
79
83
|
hash: txHash,
|
|
80
|
-
confirmations
|
|
84
|
+
confirmations,
|
|
81
85
|
})
|
|
82
86
|
|
|
83
87
|
return Credential.serialize({
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,12 @@ export const charge = Method.from({
|
|
|
37
37
|
token: z.string(),
|
|
38
38
|
/** Recipient address (the seller / payee) */
|
|
39
39
|
recipient: z.string(),
|
|
40
|
+
/**
|
|
41
|
+
* Optional payer address to enforce sender identity.
|
|
42
|
+
* When set, the server rejects payments from any other wallet.
|
|
43
|
+
* Useful when the paying wallet must match an authenticated session.
|
|
44
|
+
*/
|
|
45
|
+
payer: z.optional(z.string()),
|
|
40
46
|
}),
|
|
41
47
|
},
|
|
42
48
|
})
|
package/src/server.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function radius(config: RadiusServerConfig = {}): Method.Server<RadiusCha
|
|
|
59
59
|
return Method.toServer(charge, {
|
|
60
60
|
async verify({ credential }) {
|
|
61
61
|
const { txHash } = credential.payload
|
|
62
|
-
const { amount, chainId, token, recipient } = credential.challenge.request
|
|
62
|
+
const { amount, chainId, token, recipient, payer } = credential.challenge.request
|
|
63
63
|
|
|
64
64
|
// Resolve chain
|
|
65
65
|
const chain = resolveChain(chainId)
|
|
@@ -83,8 +83,14 @@ export function radius(config: RadiusServerConfig = {}): Method.Server<RadiusCha
|
|
|
83
83
|
throw new Error(`Transaction ${txHash} failed on-chain (status: ${receipt.status})`)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// If payer is specified, verify the tx was sent by the expected wallet
|
|
87
|
+
if (payer && receipt.from.toLowerCase() !== payer.toLowerCase()) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Transaction sender ${receipt.from} does not match expected payer ${payer}`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
// Check the tx was sent to the correct token contract
|
|
87
|
-
// TODO: Also verify receipt.from if we want to enforce payer identity
|
|
88
94
|
const txToAddress = receipt.to?.toLowerCase()
|
|
89
95
|
if (txToAddress !== token.toLowerCase()) {
|
|
90
96
|
throw new Error(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/constants.ts"],"sourcesContent":["/**\n * @stablecoin.xyz/radius-mpp\n *\n * Shared method definition for Radius ERC-20 payments via MPP.\n * Import from the root path in code that needs the schema but not\n * client or server logic (e.g. shared types).\n */\n\nimport { Method, z } from 'mppx'\n\nexport { radiusMainnet, radiusTestnet, SBC_TOKEN, SBC_DECIMALS, explorerTxUrl } from './constants.js'\n\n/**\n * Radius charge method — one-time ERC-20 token transfer on Radius.\n *\n * Request schema: the Challenge fields the server sends to the client.\n * Credential payload: what the client returns after broadcasting the tx.\n */\nexport const charge = Method.from({\n intent: 'charge',\n name: 'radius',\n schema: {\n credential: {\n payload: z.object({\n /** The on-chain transaction hash of the ERC-20 transfer */\n txHash: z.string(),\n }),\n },\n request: z.object({\n /** Transfer amount in atomic units (e.g. \"1000000\" for 1 SBC with 6 decimals) */\n amount: z.string(),\n /** Token symbol or identifier (e.g. \"SBC\") */\n currency: z.string(),\n /** Chain ID (723487 for mainnet, 72344 for testnet) */\n chainId: z.number(),\n /** ERC-20 token contract address on Radius */\n token: z.string(),\n /** Recipient address (the seller / payee) */\n recipient: z.string(),\n }),\n },\n})\n\n/** Re-export the inferred types for external consumers */\nexport type RadiusCharge = typeof charge\n","import { defineChain } from 'viem'\n\n// ---------------------------------------------------------------------------\n// Token addresses (SBC stablecoin on Radius)\n// ---------------------------------------------------------------------------\n\nexport const SBC_TOKEN = {\n mainnet: '0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb' as const,\n testnet: '0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb' as const,\n}\n\nexport const SBC_DECIMALS = 6\n\n// ---------------------------------------------------------------------------\n// Chain definitions (viem-compatible)\n// ---------------------------------------------------------------------------\n\nexport const radiusMainnet = defineChain({\n id: 723487,\n name: 'Radius',\n nativeCurrency: { name: 'RUSD', symbol: 'RUSD', decimals: 18 },\n rpcUrls: {\n default: { http: ['https://rpc.radiustech.xyz'] },\n },\n blockExplorers: {\n default: { name: 'Radius Explorer', url: 'https://network.radiustech.xyz' },\n },\n})\n\nexport const radiusTestnet = defineChain({\n id: 72344,\n name: 'Radius Testnet',\n nativeCurrency: { name: 'RUSD', symbol: 'RUSD', decimals: 18 },\n rpcUrls: {\n default: { http: ['https://rpc.testnet.radiustech.xyz'] },\n },\n blockExplorers: {\n default: { name: 'Radius Testnet Explorer', url: 'https://testnet.radiustech.xyz' },\n },\n testnet: true,\n})\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Resolve chain definition by chain ID. Returns mainnet for 723 (legacy alias). */\nexport function resolveChain(chainId: number) {\n if (chainId === radiusMainnet.id || chainId === 723) return radiusMainnet\n if (chainId === radiusTestnet.id) return radiusTestnet\n return null\n}\n\n/** Resolve the SBC token address for a given chain ID. */\nexport function resolveTokenAddress(chainId: number) {\n if (chainId === radiusMainnet.id || chainId === 723) return SBC_TOKEN.mainnet\n if (chainId === radiusTestnet.id) return SBC_TOKEN.testnet\n return null\n}\n\n/** Build an explorer URL for a transaction hash on a given chain. */\nexport function explorerTxUrl(chainId: number, txHash: string): string | null {\n const chain = resolveChain(chainId)\n if (!chain?.blockExplorers?.default) return null\n return `${chain.blockExplorers.default.url}/tx/${txHash}`\n}\n\n// Standard ERC-20 ABI subset used by client and server\nexport const ERC20_ABI = [\n {\n inputs: [\n { name: 'to', type: 'address' },\n { name: 'amount', type: 'uint256' },\n ],\n name: 'transfer',\n outputs: [{ name: '', type: 'bool' }],\n stateMutability: 'nonpayable',\n type: 'function',\n },\n {\n inputs: [{ name: 'account', type: 'address' }],\n name: 'balanceOf',\n outputs: [{ name: '', type: 'uint256' }],\n stateMutability: 'view',\n type: 'function',\n },\n] as const\n"],"mappings":";AAQA,SAAS,QAAQ,SAAS;;;ACR1B,SAAS,mBAAmB;AAMrB,IAAM,YAAY;AAAA,EACvB,SAAS;AAAA,EACT,SAAS;AACX;AAEO,IAAM,eAAe;AAMrB,IAAM,gBAAgB,YAAY;AAAA,EACvC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,EAAE,MAAM,QAAQ,QAAQ,QAAQ,UAAU,GAAG;AAAA,EAC7D,SAAS;AAAA,IACP,SAAS,EAAE,MAAM,CAAC,4BAA4B,EAAE;AAAA,EAClD;AAAA,EACA,gBAAgB;AAAA,IACd,SAAS,EAAE,MAAM,mBAAmB,KAAK,iCAAiC;AAAA,EAC5E;AACF,CAAC;AAEM,IAAM,gBAAgB,YAAY;AAAA,EACvC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,EAAE,MAAM,QAAQ,QAAQ,QAAQ,UAAU,GAAG;AAAA,EAC7D,SAAS;AAAA,IACP,SAAS,EAAE,MAAM,CAAC,oCAAoC,EAAE;AAAA,EAC1D;AAAA,EACA,gBAAgB;AAAA,IACd,SAAS,EAAE,MAAM,2BAA2B,KAAK,iCAAiC;AAAA,EACpF;AAAA,EACA,SAAS;AACX,CAAC;AAOM,SAAS,aAAa,SAAiB;AAC5C,MAAI,YAAY,cAAc,MAAM,YAAY,IAAK,QAAO;AAC5D,MAAI,YAAY,cAAc,GAAI,QAAO;AACzC,SAAO;AACT;AAUO,SAAS,cAAc,SAAiB,QAA+B;AAC5E,QAAM,QAAQ,aAAa,OAAO;AAClC,MAAI,CAAC,OAAO,gBAAgB,QAAS,QAAO;AAC5C,SAAO,GAAG,MAAM,eAAe,QAAQ,GAAG,OAAO,MAAM;AACzD;AAGO,IAAM,YAAY;AAAA,EACvB;AAAA,IACE,QAAQ;AAAA,MACN,EAAE,MAAM,MAAM,MAAM,UAAU;AAAA,MAC9B,EAAE,MAAM,UAAU,MAAM,UAAU;AAAA,IACpC;AAAA,IACA,MAAM;AAAA,IACN,SAAS,CAAC,EAAE,MAAM,IAAI,MAAM,OAAO,CAAC;AAAA,IACpC,iBAAiB;AAAA,IACjB,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,QAAQ,CAAC,EAAE,MAAM,WAAW,MAAM,UAAU,CAAC;AAAA,IAC7C,MAAM;AAAA,IACN,SAAS,CAAC,EAAE,MAAM,IAAI,MAAM,UAAU,CAAC;AAAA,IACvC,iBAAiB;AAAA,IACjB,MAAM;AAAA,EACR;AACF;;;ADpEO,IAAM,SAAS,OAAO,KAAK;AAAA,EAChC,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,IACN,YAAY;AAAA,MACV,SAAS,EAAE,OAAO;AAAA;AAAA,QAEhB,QAAQ,EAAE,OAAO;AAAA,MACnB,CAAC;AAAA,IACH;AAAA,IACA,SAAS,EAAE,OAAO;AAAA;AAAA,MAEhB,QAAQ,EAAE,OAAO;AAAA;AAAA,MAEjB,UAAU,EAAE,OAAO;AAAA;AAAA,MAEnB,SAAS,EAAE,OAAO;AAAA;AAAA,MAElB,OAAO,EAAE,OAAO;AAAA;AAAA,MAEhB,WAAW,EAAE,OAAO;AAAA,IACtB,CAAC;AAAA,EACH;AACF,CAAC;","names":[]}
|