uvd-x402-sdk 2.87.0 → 2.89.0
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 +235 -0
- package/dist/adapters/index.d.mts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/backend/index.d.mts +2 -2
- package/dist/backend/index.d.ts +2 -2
- package/dist/backend/index.js +138 -0
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/index.mjs +137 -1
- package/dist/backend/index.mjs.map +1 -1
- package/dist/erc8128/index.js.map +1 -1
- package/dist/erc8128/index.mjs.map +1 -1
- package/dist/{index-D8buGZWY.d.mts → index-13ZUDF7c.d.mts} +32 -2
- package/dist/{index-CWZUj5mz.d.ts → index-B5B9EK3x.d.ts} +32 -2
- package/dist/{index-DOBhTF-j.d.mts → index-BWN0Haa0.d.mts} +476 -2
- package/dist/{index-DOBhTF-j.d.ts → index-BWN0Haa0.d.ts} +476 -2
- package/dist/{index-BHfYxpzI.d.mts → index-CAJg-_cr.d.mts} +181 -2
- package/dist/{index-D67eNxY8.d.ts → index-Cd1Mpznb.d.ts} +181 -2
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +472 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +464 -9
- package/dist/index.mjs.map +1 -1
- package/dist/providers/algorand/index.d.mts +1 -1
- package/dist/providers/algorand/index.d.ts +1 -1
- package/dist/providers/algorand/index.js.map +1 -1
- package/dist/providers/algorand/index.mjs.map +1 -1
- package/dist/providers/evm/index.d.mts +1 -1
- package/dist/providers/evm/index.d.ts +1 -1
- package/dist/providers/evm/index.js.map +1 -1
- package/dist/providers/evm/index.mjs.map +1 -1
- package/dist/providers/near/index.d.mts +1 -1
- package/dist/providers/near/index.d.ts +1 -1
- package/dist/providers/near/index.js.map +1 -1
- package/dist/providers/near/index.mjs.map +1 -1
- package/dist/providers/solana/index.d.mts +1 -1
- package/dist/providers/solana/index.d.ts +1 -1
- package/dist/providers/solana/index.js.map +1 -1
- package/dist/providers/solana/index.mjs.map +1 -1
- package/dist/providers/stellar/index.d.mts +1 -1
- package/dist/providers/stellar/index.d.ts +1 -1
- package/dist/providers/stellar/index.js.map +1 -1
- package/dist/providers/stellar/index.mjs.map +1 -1
- package/dist/providers/sui/index.d.mts +1 -1
- package/dist/providers/sui/index.d.ts +1 -1
- package/dist/providers/sui/index.js.map +1 -1
- package/dist/providers/sui/index.mjs.map +1 -1
- package/dist/providers/xrpl/index.d.mts +1 -1
- package/dist/providers/xrpl/index.d.ts +1 -1
- package/dist/providers/xrpl/index.js.map +1 -1
- package/dist/providers/xrpl/index.mjs.map +1 -1
- package/dist/react/index.d.mts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +463 -8
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +463 -8
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/picker/index.d.mts +1 -1
- package/dist/react/picker/index.d.ts +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs.map +1 -1
- package/dist/{validation-mSLW4tcO.d.mts → validation-B95g0MqC.d.mts} +1 -1
- package/dist/{validation-C7p3a6Ep.d.ts → validation-_Tcjm2gu.d.ts} +1 -1
- package/package.json +1 -1
- package/src/backend/index.ts +260 -0
- package/src/client/X402Client.ts +232 -9
- package/src/index.ts +28 -0
- package/src/policy.ts +804 -0
- package/src/types/index.ts +41 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { j as X402Header, X as X402Version, C as ChainConfig, k as X402PayloadData, l as X402HeaderV1, m as X402PaymentOption, n as X402HeaderV2, T as TokenType, o as PaymentTokenMetadata, p as X402HeaderName, q as PaymentHeaders } from './index-BWN0Haa0.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* uvd-x402-sdk - x402 Protocol Utilities
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { j as X402Header, X as X402Version, C as ChainConfig, k as X402PayloadData, l as X402HeaderV1, m as X402PaymentOption, n as X402HeaderV2, T as TokenType, o as PaymentTokenMetadata, p as X402HeaderName, q as PaymentHeaders } from './index-BWN0Haa0.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* uvd-x402-sdk - x402 Protocol Utilities
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uvd-x402-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.89.0",
|
|
4
4
|
"description": "x402 Payment SDK - Gasless crypto payments across 25 blockchains via Ultravioleta facilitator. Supports EVM (including Scroll, SKALE Base, Robinhood Chain), Solana, Fogo, Stellar, NEAR, Algorand, Sui, and XRP Ledger. Features: ERC-8004 Trustless Agents, Escrow/Refunds, multi-stablecoin (USDC, EURC, AUSD, PYUSD, USDT, USDG).",
|
|
5
5
|
"author": "Ultravioleta DAO <ultravioletadao@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/backend/index.ts
CHANGED
|
@@ -3685,6 +3685,42 @@ export function supportsRelayedFeedback(network: string): boolean {
|
|
|
3685
3685
|
return (RELAYED_FEEDBACK_NETWORKS as readonly string[]).includes(wireNetwork(network));
|
|
3686
3686
|
}
|
|
3687
3687
|
|
|
3688
|
+
/**
|
|
3689
|
+
* Networks where the facilitator serves the rater-authored feedback rail on
|
|
3690
|
+
* **Solana**, through `POST /feedback/solana/prepare` + `/submit`.
|
|
3691
|
+
*
|
|
3692
|
+
* A separate list from {@link RELAYED_FEEDBACK_NETWORKS} on purpose, and the
|
|
3693
|
+
* two must never be merged. That one names the chains where Execution Market
|
|
3694
|
+
* deployed a `FeedbackDelegate` and the facilitator verified it on-chain; it
|
|
3695
|
+
* routes to `/feedback/evm/*`. Adding `solana` to it would send a Solana
|
|
3696
|
+
* rating to the EVM route, which answers 400 — and would claim a delegate that
|
|
3697
|
+
* does not exist and is not missing.
|
|
3698
|
+
*
|
|
3699
|
+
* Solana needs no delegate at all: account 0 of the program's `give_feedback`
|
|
3700
|
+
* instruction is already declared `[signer, writable] client`, so the rater
|
|
3701
|
+
* signs as the author natively and the facilitator only co-signs as fee payer.
|
|
3702
|
+
* Nothing is delegated because nothing has to be.
|
|
3703
|
+
*
|
|
3704
|
+
* Both networks are served by the deployed facilitator (v2.16.0, measured
|
|
3705
|
+
* against `GET /supported`).
|
|
3706
|
+
*/
|
|
3707
|
+
export const SOLANA_FEEDBACK_NETWORKS: readonly Erc8004Network[] = [
|
|
3708
|
+
'solana',
|
|
3709
|
+
'solana-devnet',
|
|
3710
|
+
] as const;
|
|
3711
|
+
|
|
3712
|
+
/**
|
|
3713
|
+
* Whether `network` serves the rater-authored feedback rail on Solana.
|
|
3714
|
+
*
|
|
3715
|
+
* A routing hint, never the authority: the facilitator re-resolves the
|
|
3716
|
+
* programs and the RPC provider per request and answers
|
|
3717
|
+
* `400 "<network> is not a Solana network served by this facilitator"` for
|
|
3718
|
+
* anything else.
|
|
3719
|
+
*/
|
|
3720
|
+
export function supportsSolanaFeedback(network: string): boolean {
|
|
3721
|
+
return (SOLANA_FEEDBACK_NETWORKS as readonly string[]).includes(wireNetwork(network));
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3688
3724
|
/**
|
|
3689
3725
|
* An EIP-7702 authorization, as a wallet produces it.
|
|
3690
3726
|
*
|
|
@@ -3821,6 +3857,84 @@ export interface SubmitRelayFeedbackRequest {
|
|
|
3821
3857
|
authorization?: RelayAuthorizationParams;
|
|
3822
3858
|
}
|
|
3823
3859
|
|
|
3860
|
+
/**
|
|
3861
|
+
* Request body for `POST /feedback/solana/prepare`.
|
|
3862
|
+
*
|
|
3863
|
+
* `rater` is REQUIRED here and is a base58 Solana pubkey — an `0x` address is
|
|
3864
|
+
* refused (`400 "rater must be a base58 Solana pubkey on this network"`).
|
|
3865
|
+
* `agentId` is the agent's asset pubkey, also base58.
|
|
3866
|
+
*/
|
|
3867
|
+
export interface PrepareSolanaFeedbackRequest {
|
|
3868
|
+
x402Version: 1 | 2;
|
|
3869
|
+
/** One of {@link SOLANA_FEEDBACK_NETWORKS} */
|
|
3870
|
+
network: Erc8004Network;
|
|
3871
|
+
/**
|
|
3872
|
+
* The rating, plus who signs it.
|
|
3873
|
+
*
|
|
3874
|
+
* **Set `score`.** It is optional on the wire and the ATOM Engine ignores an
|
|
3875
|
+
* unscored feedback entirely: the record lands on the agent, the transaction
|
|
3876
|
+
* succeeds, and the program reports `had_impact=false` — reputation stays at
|
|
3877
|
+
* zero however much unscored feedback accumulates, and it is not retroactive.
|
|
3878
|
+
* Omit it only when a deliberately non-scoring record is what you want.
|
|
3879
|
+
*/
|
|
3880
|
+
feedback: FeedbackParams & { rater: string };
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
/**
|
|
3884
|
+
* Response from `POST /feedback/solana/prepare`.
|
|
3885
|
+
*
|
|
3886
|
+
* Carries an UNSIGNED transaction whose `client` account is the rater. Sign it
|
|
3887
|
+
* with the rater's key and hand it back to {@link Erc8004Client.submitSolanaFeedback}.
|
|
3888
|
+
*/
|
|
3889
|
+
export interface PrepareSolanaFeedbackResponse {
|
|
3890
|
+
success: boolean;
|
|
3891
|
+
/**
|
|
3892
|
+
* base64 of the bincode-serialised unsigned **legacy** transaction — read it
|
|
3893
|
+
* with `Transaction.from()`, not `VersionedTransaction`.
|
|
3894
|
+
*
|
|
3895
|
+
* It expects TWO signatures: the rater's, which you add, and the fee
|
|
3896
|
+
* payer's, which `/feedback/solana/submit` adds. Do not re-encode the
|
|
3897
|
+
* message or rebuild the instruction — the facilitator rebuilds it from the
|
|
3898
|
+
* declared parameters and refuses to co-sign anything that is not
|
|
3899
|
+
* byte-for-byte what it offered.
|
|
3900
|
+
*/
|
|
3901
|
+
transaction?: string;
|
|
3902
|
+
/** Who must sign as `client`, i.e. who the chain will record as the author */
|
|
3903
|
+
rater?: string;
|
|
3904
|
+
/** Who pays the fee. Still the facilitator — that is the point */
|
|
3905
|
+
feePayer?: string;
|
|
3906
|
+
/** The blockhash baked into the message. Submit before it expires */
|
|
3907
|
+
blockhash?: string;
|
|
3908
|
+
/**
|
|
3909
|
+
* Last block height at which this transaction is still valid.
|
|
3910
|
+
*
|
|
3911
|
+
* Past it the network drops the transaction; nothing is written and nothing
|
|
3912
|
+
* is charged. Prepare again rather than resubmitting.
|
|
3913
|
+
*/
|
|
3914
|
+
lastValidBlockHeight?: number;
|
|
3915
|
+
error?: string;
|
|
3916
|
+
network: Erc8004Network;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
/**
|
|
3920
|
+
* Request body for `POST /feedback/solana/submit`.
|
|
3921
|
+
*
|
|
3922
|
+
* The feedback parameters are not redundant with `prepare`: the facilitator
|
|
3923
|
+
* re-derives the message from them plus the blockhash carried by
|
|
3924
|
+
* {@link transaction}, and refuses to co-sign anything that does not match
|
|
3925
|
+
* byte for byte. Signing arbitrary blobs would turn the fee-payer keypair into
|
|
3926
|
+
* a public signing oracle — one `system_program::transfer` would empty the
|
|
3927
|
+
* wallet with the facilitator's signature on it.
|
|
3928
|
+
*/
|
|
3929
|
+
export interface SubmitSolanaFeedbackRequest {
|
|
3930
|
+
x402Version: 1 | 2;
|
|
3931
|
+
network: Erc8004Network;
|
|
3932
|
+
/** Exactly what was sent to `prepare`, `rater` included */
|
|
3933
|
+
feedback: FeedbackParams & { rater: string };
|
|
3934
|
+
/** base64 of the rater-signed transaction */
|
|
3935
|
+
transaction: string;
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3824
3938
|
/**
|
|
3825
3939
|
* Request body for `POST /feedback/response/evm/prepare`.
|
|
3826
3940
|
*
|
|
@@ -4876,6 +4990,152 @@ export class Erc8004Client {
|
|
|
4876
4990
|
}
|
|
4877
4991
|
}
|
|
4878
4992
|
|
|
4993
|
+
/**
|
|
4994
|
+
* Ask the facilitator for the Solana transaction the rater must sign.
|
|
4995
|
+
*
|
|
4996
|
+
* Step 1 of the rater-authored rail on Solana. Writes nothing on-chain and
|
|
4997
|
+
* costs nothing: it resolves the programs and the agent asset, reads the
|
|
4998
|
+
* registry collection and a finalized blockhash, and hands back an UNSIGNED
|
|
4999
|
+
* transaction whose `client` account is the rater.
|
|
5000
|
+
*
|
|
5001
|
+
* Why this exists, and why it is not the EVM rail: the program's
|
|
5002
|
+
* `give_feedback` instruction declares account 0 as
|
|
5003
|
+
* `[signer, writable] client (feedback author / fee payer)`, and plain
|
|
5004
|
+
* `POST /feedback` puts the FACILITATOR's keypair there — so the chain
|
|
5005
|
+
* records the facilitator as the author of the rating. Solana carries several
|
|
5006
|
+
* signers per transaction natively, so the rater signs as `client` while the
|
|
5007
|
+
* facilitator stays the fee payer. **No delegation and no program change**,
|
|
5008
|
+
* which is why {@link SOLANA_FEEDBACK_NETWORKS} is its own list and `solana`
|
|
5009
|
+
* must never appear in {@link RELAYED_FEEDBACK_NETWORKS} — that one routes to
|
|
5010
|
+
* `/feedback/evm/*`, which answers 400 here.
|
|
5011
|
+
*
|
|
5012
|
+
* What to do with the answer:
|
|
5013
|
+
* 1. Deserialise `transaction` — base64 of the bincode-serialised **legacy**
|
|
5014
|
+
* transaction, which is `@solana/web3.js`'s `Transaction.from()`, not
|
|
5015
|
+
* `VersionedTransaction` — and add the RATER's ed25519 signature. Sign the
|
|
5016
|
+
* message as it came: re-encoding it changes bytes the facilitator will
|
|
5017
|
+
* compare. The fee payer's slot stays empty; `submit` fills it.
|
|
5018
|
+
* 2. Hand it back to {@link Erc8004Client.submitSolanaFeedback} with the SAME
|
|
5019
|
+
* feedback parameters, before `lastValidBlockHeight` passes.
|
|
5020
|
+
*
|
|
5021
|
+
* @param request - Network, rater pubkey and feedback parameters
|
|
5022
|
+
* @returns The unsigned transaction, the fee payer and the blockhash window
|
|
5023
|
+
*
|
|
5024
|
+
* @example
|
|
5025
|
+
* ```ts
|
|
5026
|
+
* import { Transaction } from '@solana/web3.js';
|
|
5027
|
+
*
|
|
5028
|
+
* const prep = await erc8004.prepareSolanaFeedback({
|
|
5029
|
+
* x402Version: 1,
|
|
5030
|
+
* network: 'solana',
|
|
5031
|
+
* feedback: {
|
|
5032
|
+
* agentId: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgHkv',
|
|
5033
|
+
* rater: raterPubkey, // base58: who the chain records as the author
|
|
5034
|
+
* value: 87,
|
|
5035
|
+
* score: 95, // without it the rating counts for nothing
|
|
5036
|
+
* tag1: 'quality',
|
|
5037
|
+
* },
|
|
5038
|
+
* });
|
|
5039
|
+
*
|
|
5040
|
+
* const tx = Transaction.from(Buffer.from(prep.transaction!, 'base64'));
|
|
5041
|
+
* tx.partialSign(raterKeypair);
|
|
5042
|
+
* ```
|
|
5043
|
+
*/
|
|
5044
|
+
async prepareSolanaFeedback(
|
|
5045
|
+
request: PrepareSolanaFeedbackRequest,
|
|
5046
|
+
): Promise<PrepareSolanaFeedbackResponse> {
|
|
5047
|
+
const url = `${this.baseUrl}/feedback/solana/prepare`;
|
|
5048
|
+
|
|
5049
|
+
const controller = new AbortController();
|
|
5050
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
5051
|
+
|
|
5052
|
+
try {
|
|
5053
|
+
const response = await fetch(url, {
|
|
5054
|
+
method: 'POST',
|
|
5055
|
+
headers: {
|
|
5056
|
+
'Content-Type': 'application/json',
|
|
5057
|
+
'Accept': 'application/json',
|
|
5058
|
+
},
|
|
5059
|
+
body: JSON.stringify({ ...request, network: wireNetwork(request.network) }),
|
|
5060
|
+
signal: controller.signal,
|
|
5061
|
+
});
|
|
5062
|
+
|
|
5063
|
+
clearTimeout(timeoutId);
|
|
5064
|
+
|
|
5065
|
+
if (!response.ok) {
|
|
5066
|
+
const errorText = await response.text();
|
|
5067
|
+
return {
|
|
5068
|
+
success: false,
|
|
5069
|
+
error: `Facilitator error: ${response.status} - ${errorText}`,
|
|
5070
|
+
network: request.network,
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
return await response.json();
|
|
5075
|
+
} catch (error) {
|
|
5076
|
+
clearTimeout(timeoutId);
|
|
5077
|
+
return {
|
|
5078
|
+
success: false,
|
|
5079
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
5080
|
+
network: request.network,
|
|
5081
|
+
};
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
/**
|
|
5086
|
+
* Co-sign and send a rater-signed Solana feedback transaction.
|
|
5087
|
+
*
|
|
5088
|
+
* Step 2 of the rater-authored rail on Solana. What lands on-chain has the
|
|
5089
|
+
* RATER as `client`, so the agent's feedback lists the rater rather than the
|
|
5090
|
+
* facilitator — and only the rater can revoke it.
|
|
5091
|
+
*
|
|
5092
|
+
* Pass back the same feedback parameters {@link Erc8004Client.prepareSolanaFeedback}
|
|
5093
|
+
* was given. They are not redundant: the facilitator re-derives the message
|
|
5094
|
+
* from them plus the blockhash inside the submitted transaction, and refuses
|
|
5095
|
+
* to co-sign anything that is not byte-for-byte what it built
|
|
5096
|
+
* (`400 "submitted transaction does not match the one this facilitator built"`).
|
|
5097
|
+
* The rater's signature is verified BEFORE the facilitator adds its own, so a
|
|
5098
|
+
* transaction the network would reject never costs a fee.
|
|
5099
|
+
*
|
|
5100
|
+
* @param request - Feedback parameters plus the rater-signed transaction
|
|
5101
|
+
* @returns Feedback response with the transaction signature
|
|
5102
|
+
*/
|
|
5103
|
+
async submitSolanaFeedback(
|
|
5104
|
+
request: SubmitSolanaFeedbackRequest,
|
|
5105
|
+
): Promise<FeedbackResponse> {
|
|
5106
|
+
const url = `${this.baseUrl}/feedback/solana/submit`;
|
|
5107
|
+
|
|
5108
|
+
try {
|
|
5109
|
+
const { response, error } = await this.writeJson(url, {
|
|
5110
|
+
...request,
|
|
5111
|
+
network: wireNetwork(request.network),
|
|
5112
|
+
});
|
|
5113
|
+
|
|
5114
|
+
if (error) {
|
|
5115
|
+
return {
|
|
5116
|
+
success: false,
|
|
5117
|
+
error: error.error,
|
|
5118
|
+
network: request.network,
|
|
5119
|
+
...failureFields(error),
|
|
5120
|
+
};
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
return await response.json();
|
|
5124
|
+
} catch (error) {
|
|
5125
|
+
// Timeout or connection failure: the write may already be on-chain.
|
|
5126
|
+
// Retryable, never replayable -- and on this rail a resubmission needs a
|
|
5127
|
+
// fresh `prepare`, because the blockhash the rater signed over expires.
|
|
5128
|
+
return {
|
|
5129
|
+
success: false,
|
|
5130
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
5131
|
+
network: request.network,
|
|
5132
|
+
retryable: true,
|
|
5133
|
+
safeToReplay: false,
|
|
5134
|
+
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS,
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
|
|
4879
5139
|
/**
|
|
4880
5140
|
* Revoke previously submitted feedback
|
|
4881
5141
|
*
|
package/src/client/X402Client.ts
CHANGED
|
@@ -25,6 +25,14 @@ import type {
|
|
|
25
25
|
WalletAdapter,
|
|
26
26
|
} from '../types';
|
|
27
27
|
import { X402Error, DEFAULT_CONFIG } from '../types';
|
|
28
|
+
import {
|
|
29
|
+
PurchasePolicy,
|
|
30
|
+
PolicyRefusedError,
|
|
31
|
+
decideOnChallenge,
|
|
32
|
+
noReadableOffer,
|
|
33
|
+
canonicalRecipient,
|
|
34
|
+
} from '../policy';
|
|
35
|
+
import type { PolicyApproval, ReadChallenge } from '../policy';
|
|
28
36
|
import {
|
|
29
37
|
SUPPORTED_CHAINS,
|
|
30
38
|
getChainByName,
|
|
@@ -40,6 +48,52 @@ import {
|
|
|
40
48
|
buildTokenMetadata,
|
|
41
49
|
} from '../utils';
|
|
42
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The x402 scheme vocabulary, shared by all three implementations.
|
|
53
|
+
*
|
|
54
|
+
* The same five the Rust facilitator's closed `Scheme` enum holds and the same
|
|
55
|
+
* five the Python SDK keys `KNOWN_SCHEMES` on. A scheme outside this set is one
|
|
56
|
+
* no implementation in the stack can name, so an entry carrying one is unreadable
|
|
57
|
+
* and is counted by the name the seller used.
|
|
58
|
+
*
|
|
59
|
+
* Kept as a set of known NAMES rather than of payable ones because the two are
|
|
60
|
+
* different questions, and conflating them is what rule 7 is about: naming what a
|
|
61
|
+
* seller offered is how a caller learns to go find a facilitator that implements
|
|
62
|
+
* it. See {@link CLIENT_PAYABLE_SCHEMES} for the other question.
|
|
63
|
+
*/
|
|
64
|
+
export const KNOWN_SCHEMES: ReadonlySet<string> = new Set([
|
|
65
|
+
'exact',
|
|
66
|
+
'upto',
|
|
67
|
+
'escrow',
|
|
68
|
+
'commerce',
|
|
69
|
+
'fhe-transfer',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Of those, the ones the BUYER path in this client can actually sign.
|
|
74
|
+
*
|
|
75
|
+
* One entry, and that is the point: the payload builder stamps
|
|
76
|
+
* `scheme: 'exact'` into everything it produces, so `exact` is the only thing
|
|
77
|
+
* this client can honestly present. `escrow` and `commerce` are implemented on
|
|
78
|
+
* the SELLER side (`src/backend`) and in the pre-auth builder, and `upto` and
|
|
79
|
+
* `fhe-transfer` are not implemented here at all.
|
|
80
|
+
*
|
|
81
|
+
* # Why this is a SECOND set and not a narrower first one
|
|
82
|
+
*
|
|
83
|
+
* `parse402` used to accept any entry carrying an amount, a payee and a network,
|
|
84
|
+
* so a well-formed offer asking for a scheme this build cannot present was read
|
|
85
|
+
* as payable and then **signed as `exact`** -- a payment offered to the seller
|
|
86
|
+
* under a scheme it never asked for. Recognising a scheme is not being able to
|
|
87
|
+
* pay it. Collapsing these two sets into one re-opens exactly that hole: widen
|
|
88
|
+
* this one to the five known names and a well-formed `escrow` offer gets signed
|
|
89
|
+
* as `exact` again.
|
|
90
|
+
*
|
|
91
|
+
* Both cases end the same way for the caller -- the entry is excluded and counted
|
|
92
|
+
* by its scheme name -- and `no-readable-offer` says "no offer in this challenge
|
|
93
|
+
* is one this build **can pay**", which is the frame both of them sit in.
|
|
94
|
+
*/
|
|
95
|
+
export const CLIENT_PAYABLE_SCHEMES: ReadonlySet<string> = new Set(['exact']);
|
|
96
|
+
|
|
43
97
|
/**
|
|
44
98
|
* X402Client - Main SDK client for x402 payments
|
|
45
99
|
*
|
|
@@ -82,6 +136,17 @@ export class X402Client {
|
|
|
82
136
|
// Event emitter
|
|
83
137
|
private eventHandlers: Map<X402Event, Set<X402EventHandler<X402Event>>> = new Map();
|
|
84
138
|
|
|
139
|
+
/**
|
|
140
|
+
* What this buyer is allowed to sign, evaluated against the offer in hand
|
|
141
|
+
* before anything is signed.
|
|
142
|
+
*
|
|
143
|
+
* `permissive()` when the caller supplied none, and that asymmetry is
|
|
144
|
+
* deliberate: this SDK had no budget before 2.89.0, so switching one on
|
|
145
|
+
* silently would refuse payments consumers are making today, while whoever
|
|
146
|
+
* sits down to WRITE a policy gets the deny-by-default one.
|
|
147
|
+
*/
|
|
148
|
+
private readonly purchasePolicy: PurchasePolicy;
|
|
149
|
+
|
|
85
150
|
constructor(config: X402ClientConfig = {}) {
|
|
86
151
|
this.config = {
|
|
87
152
|
...DEFAULT_CONFIG,
|
|
@@ -106,9 +171,21 @@ export class X402Client {
|
|
|
106
171
|
}
|
|
107
172
|
}
|
|
108
173
|
|
|
174
|
+
this.purchasePolicy = config.policy ?? PurchasePolicy.permissive();
|
|
175
|
+
|
|
109
176
|
this.log('X402Client initialized', { config: this.config });
|
|
110
177
|
}
|
|
111
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The policy in force, for a caller that wants to record a settled payment
|
|
181
|
+
* against it with {@link PurchasePolicy.recordSpend}.
|
|
182
|
+
*
|
|
183
|
+
* Nothing on the payment path ever widens it.
|
|
184
|
+
*/
|
|
185
|
+
get policy(): PurchasePolicy {
|
|
186
|
+
return this.purchasePolicy;
|
|
187
|
+
}
|
|
188
|
+
|
|
112
189
|
// ============================================================================
|
|
113
190
|
// PUBLIC API - Wallet Connection
|
|
114
191
|
// ============================================================================
|
|
@@ -375,8 +452,19 @@ export class X402Client {
|
|
|
375
452
|
}
|
|
376
453
|
|
|
377
454
|
const tokenType: TokenType = options.tokenType || 'usdc';
|
|
378
|
-
const
|
|
455
|
+
const challenge = this.parse402(body, tokenType);
|
|
456
|
+
const { version, offers } = challenge;
|
|
379
457
|
if (offers.length === 0) {
|
|
458
|
+
// Step 1 of the policy contract, and two different facts that used to be
|
|
459
|
+
// one error. A seller that sent offers none of which this build can read is
|
|
460
|
+
// asking for a scheme we do not implement; saying so names what it wanted,
|
|
461
|
+
// where "no usable payment options" sends the caller hunting a bug in its
|
|
462
|
+
// own code. A seller that sent none at all is the other fact, and keeps the
|
|
463
|
+
// error it always had.
|
|
464
|
+
const unreadableCount = challenge.unreadableCount ?? challenge.unreadable.length;
|
|
465
|
+
if (unreadableCount > 0) {
|
|
466
|
+
throw new PolicyRefusedError(noReadableOffer(challenge.unreadable));
|
|
467
|
+
}
|
|
380
468
|
throw new X402Error(
|
|
381
469
|
'402 response offered no usable payment options',
|
|
382
470
|
'NO_ACCEPTABLE_PAYMENT'
|
|
@@ -412,6 +500,33 @@ export class X402Client {
|
|
|
412
500
|
}
|
|
413
501
|
}
|
|
414
502
|
|
|
503
|
+
// The policy, evaluated against THIS offer, before anything is signed. An
|
|
504
|
+
// offer that diverges from a listing but sits inside an authorised policy
|
|
505
|
+
// proceeds -- stopping to ask would turn every ordinary reprice into a halt --
|
|
506
|
+
// and one that does not is refused with a concrete cause. The policy is never
|
|
507
|
+
// widened to fit the offer, and there is no confirmation hook in this path.
|
|
508
|
+
//
|
|
509
|
+
// The whole challenge goes in, never just the offer: `validUntil` lives in
|
|
510
|
+
// the challenge's `extensions`, and a call that takes the parts is how it gets
|
|
511
|
+
// dropped.
|
|
512
|
+
const decision = decideOnChallenge(this.purchasePolicy, challenge, chosen, {
|
|
513
|
+
now: Math.floor(Date.now() / 1000),
|
|
514
|
+
quote: options.advertised,
|
|
515
|
+
});
|
|
516
|
+
if (!decision.ok) {
|
|
517
|
+
this.log('policy refused this offer', { cause: decision.refusal.code });
|
|
518
|
+
this.emit('paymentFailed', {
|
|
519
|
+
error: decision.refusal.message,
|
|
520
|
+
code: 'POLICY_REFUSED',
|
|
521
|
+
});
|
|
522
|
+
throw new PolicyRefusedError(decision.refusal);
|
|
523
|
+
}
|
|
524
|
+
const approval: PolicyApproval = decision.approval;
|
|
525
|
+
this.log('policy approved this offer', {
|
|
526
|
+
versusQuote: approval.versusQuote.code,
|
|
527
|
+
amount: String(approval.amount),
|
|
528
|
+
});
|
|
529
|
+
|
|
415
530
|
if (!chosen.chainName) {
|
|
416
531
|
throw new X402Error(
|
|
417
532
|
`402 asked for payment on unknown network '${chosen.network}'`,
|
|
@@ -419,6 +534,38 @@ export class X402Client {
|
|
|
419
534
|
);
|
|
420
535
|
}
|
|
421
536
|
|
|
537
|
+
// The policy just approved the offer's OWN asset, but `createPayment` signs
|
|
538
|
+
// for whatever token `tokenType` resolves to on this chain -- and reads the
|
|
539
|
+
// price at that token's decimals. With USDC on both sides they coincide,
|
|
540
|
+
// which is why this never showed. They do not have to: a seller pricing in a
|
|
541
|
+
// token that is not the configured one would get an approval for asset A and
|
|
542
|
+
// a signature for asset B, at B's decimals, and the caller would believe a
|
|
543
|
+
// budget it never actually enforced.
|
|
544
|
+
//
|
|
545
|
+
// Refused rather than silently re-pointed: which token to pay with is the
|
|
546
|
+
// caller's decision, and guessing it from the seller's 402 is how a wallet
|
|
547
|
+
// ends up signing for a token nobody chose.
|
|
548
|
+
const configuredToken = getTokenConfig(chosen.chainName, tokenType);
|
|
549
|
+
if (
|
|
550
|
+
chosen.asset &&
|
|
551
|
+
configuredToken &&
|
|
552
|
+
canonicalRecipient(configuredToken.address) !== canonicalRecipient(chosen.asset)
|
|
553
|
+
) {
|
|
554
|
+
throw new X402Error(
|
|
555
|
+
`402 wants ${chosen.asset} on ${chosen.chainName}, but this client signs ` +
|
|
556
|
+
`${configuredToken.address} (tokenType '${tokenType}'). Pass the tokenType ` +
|
|
557
|
+
'that matches the offer; signing the configured token would pay a ' +
|
|
558
|
+
'different asset than the one that was approved.',
|
|
559
|
+
'NO_ACCEPTABLE_PAYMENT',
|
|
560
|
+
{
|
|
561
|
+
offered: chosen.asset,
|
|
562
|
+
wouldSign: configuredToken.address,
|
|
563
|
+
tokenType,
|
|
564
|
+
network: chosen.chainName,
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
422
569
|
if (chosen.chainName !== this.currentChainName) {
|
|
423
570
|
await this.switchChain(chosen.chainName);
|
|
424
571
|
}
|
|
@@ -442,20 +589,43 @@ export class X402Client {
|
|
|
442
589
|
paidHeaders['PAYMENT-SIGNATURE'] = payment.paymentHeader;
|
|
443
590
|
}
|
|
444
591
|
|
|
445
|
-
|
|
592
|
+
const paid = await doFetch(url, { ...options.init, method, headers: paidHeaders });
|
|
593
|
+
|
|
594
|
+
// Approving did not spend: signing can fail and a settlement can be refused,
|
|
595
|
+
// and a cumulative limit that counted attempts would lock a caller out of
|
|
596
|
+
// money it never spent. A retry that came back as anything but another 402 is
|
|
597
|
+
// the seller accepting the payment, which is the moment a caller records it --
|
|
598
|
+
// by its own hand, in `onPaid`, never automatically here.
|
|
599
|
+
if (paid.status !== 402) {
|
|
600
|
+
options.onPaid?.(approval);
|
|
601
|
+
}
|
|
602
|
+
return paid;
|
|
446
603
|
}
|
|
447
604
|
|
|
448
605
|
/**
|
|
449
|
-
* Normalise a 402 body into its envelope version
|
|
606
|
+
* Normalise a 402 body into its envelope version, its payment offers, the ones
|
|
607
|
+
* it could not read, and the challenge's `extensions`.
|
|
450
608
|
*
|
|
451
609
|
* Reads both spec shapes -- `{x402Version, accepts: [...]}` and the non-spec
|
|
452
610
|
* one where a lone requirement sits at the top level -- and both dialects of
|
|
453
611
|
* the price field (`maxAmountRequired` in v1, `amount` in v2).
|
|
612
|
+
*
|
|
613
|
+
* **`accepts` is a list, so one unreadable entry does not sink the list.** A
|
|
614
|
+
* seller offering `exact` alongside a scheme this build does not implement used
|
|
615
|
+
* to be unpayable in the Rust crate, and the buyer never learned there was a
|
|
616
|
+
* perfectly payable offer right there. The readable ones are kept and the
|
|
617
|
+
* others are counted by scheme name, so a refusal can say what the seller
|
|
618
|
+
* offered instead of "could not parse". Discovering the service keeps working
|
|
619
|
+
* even when buying it automatically does not.
|
|
620
|
+
*
|
|
621
|
+
* Returns the WHOLE challenge on purpose: handing `offers` alone downstream is
|
|
622
|
+
* what dropped the seller's `validUntil` on the floor in Rust for a full commit
|
|
623
|
+
* with every unit test green.
|
|
454
624
|
*/
|
|
455
625
|
private parse402(
|
|
456
626
|
body: unknown,
|
|
457
627
|
tokenType: TokenType
|
|
458
|
-
): { version: X402Version
|
|
628
|
+
): ReadChallenge & { version: X402Version } {
|
|
459
629
|
const doc = (body ?? {}) as Record<string, unknown>;
|
|
460
630
|
const version: X402Version = Number(doc.x402Version) === 2 ? 2 : 1;
|
|
461
631
|
|
|
@@ -466,16 +636,64 @@ export class X402Client {
|
|
|
466
636
|
: [];
|
|
467
637
|
|
|
468
638
|
const offers: X402PaymentOffer[] = [];
|
|
639
|
+
const unreadable: string[] = [];
|
|
640
|
+
let unreadableCount = 0;
|
|
641
|
+
|
|
642
|
+
/** Count an entry this build cannot pay, by scheme name when it has one. */
|
|
643
|
+
const cannotRead = (accept: Record<string, unknown> | null) => {
|
|
644
|
+
unreadableCount += 1;
|
|
645
|
+
const scheme = accept?.scheme;
|
|
646
|
+
if (typeof scheme === 'string' && scheme) unreadable.push(scheme);
|
|
647
|
+
};
|
|
648
|
+
|
|
469
649
|
for (const entry of rawAccepts) {
|
|
470
|
-
if (!entry || typeof entry !== 'object')
|
|
650
|
+
if (!entry || typeof entry !== 'object') {
|
|
651
|
+
cannotRead(null);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
471
654
|
const accept = entry as Record<string, unknown>;
|
|
472
655
|
|
|
656
|
+
// The scheme first, so a well-formed offer in a scheme we cannot pay is
|
|
657
|
+
// counted by the NAME the seller used -- the informative half of rule 7 --
|
|
658
|
+
// instead of being reported for whichever field it also happened to omit.
|
|
659
|
+
//
|
|
660
|
+
// An ABSENT scheme is unreadable, not `exact`. Rust requires the field, so a
|
|
661
|
+
// challenge without it fails to deserialise there, and a buyer that guessed
|
|
662
|
+
// `exact` would sign under a scheme the seller never actually named -- which
|
|
663
|
+
// is the same hole as signing a named one we cannot present, minus the
|
|
664
|
+
// evidence. It is counted (`unreadableCount`) without a name, because it has
|
|
665
|
+
// none to report.
|
|
666
|
+
//
|
|
667
|
+
// Note this is deliberately ASYMMETRIC with the seller side of this SDK,
|
|
668
|
+
// where a missing scheme reads as `exact` ("the default, not an override",
|
|
669
|
+
// `src/backend/index.ts`). A seller is lenient about what it accepts; a
|
|
670
|
+
// buyer is strict about what it signs.
|
|
671
|
+
const declaredScheme = accept.scheme;
|
|
672
|
+
if (
|
|
673
|
+
typeof declaredScheme !== 'string' ||
|
|
674
|
+
!declaredScheme ||
|
|
675
|
+
!KNOWN_SCHEMES.has(declaredScheme) ||
|
|
676
|
+
!CLIENT_PAYABLE_SCHEMES.has(declaredScheme)
|
|
677
|
+
) {
|
|
678
|
+
cannotRead(accept);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
473
682
|
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
474
683
|
const payTo = accept.payTo;
|
|
475
684
|
const network = accept.network;
|
|
476
|
-
if (amount === undefined || amount === null)
|
|
477
|
-
|
|
478
|
-
|
|
685
|
+
if (amount === undefined || amount === null) {
|
|
686
|
+
cannotRead(accept);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (typeof payTo !== 'string' || !payTo) {
|
|
690
|
+
cannotRead(accept);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (typeof network !== 'string' || !network) {
|
|
694
|
+
cannotRead(accept);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
479
697
|
|
|
480
698
|
const chainName = getChainByName(network) ? network : caip2ToChain(network);
|
|
481
699
|
|
|
@@ -490,7 +708,12 @@ export class X402Client {
|
|
|
490
708
|
});
|
|
491
709
|
}
|
|
492
710
|
|
|
493
|
-
|
|
711
|
+
const extensions =
|
|
712
|
+
doc.extensions && typeof doc.extensions === 'object' && !Array.isArray(doc.extensions)
|
|
713
|
+
? (doc.extensions as Record<string, unknown>)
|
|
714
|
+
: undefined;
|
|
715
|
+
|
|
716
|
+
return { version, offers, unreadable, unreadableCount, extensions };
|
|
494
717
|
}
|
|
495
718
|
|
|
496
719
|
/**
|
package/src/index.ts
CHANGED
|
@@ -156,6 +156,34 @@ export type { AnchoredEvidence, EvidenceMode, RecipientRole, AnchorOptions } fro
|
|
|
156
156
|
|
|
157
157
|
export { X402Client } from './client';
|
|
158
158
|
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// Buyer policy - what this buyer is allowed to sign, decided before it signs
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
PurchasePolicy,
|
|
165
|
+
PolicyRefusedError,
|
|
166
|
+
decideOnChallenge,
|
|
167
|
+
noReadableOffer,
|
|
168
|
+
offerValidUntil,
|
|
169
|
+
offerAsset,
|
|
170
|
+
canonicalRecipient,
|
|
171
|
+
assetKey,
|
|
172
|
+
OFFER_VALIDITY_EXTENSION,
|
|
173
|
+
} from './policy';
|
|
174
|
+
export type {
|
|
175
|
+
PolicyAsset,
|
|
176
|
+
PolicyRefusal,
|
|
177
|
+
PolicyRefusalCode,
|
|
178
|
+
PolicyApproval,
|
|
179
|
+
PolicyDecision,
|
|
180
|
+
QuoteComparison,
|
|
181
|
+
AdvertisedQuote,
|
|
182
|
+
EvaluateOptions,
|
|
183
|
+
DecideOptions,
|
|
184
|
+
ReadChallenge,
|
|
185
|
+
} from './policy';
|
|
186
|
+
|
|
159
187
|
// Chain configuration
|
|
160
188
|
export {
|
|
161
189
|
SUPPORTED_CHAINS,
|