1- import { X402Canister , PaymentRequirementsResponse } from '@ldclabs/anda_x402'
1+ import {
2+ X402Canister ,
3+ PaymentRequirementsResponse ,
4+ formatAmount
5+ } from '@ldclabs/anda_x402'
26import { Ed25519KeyIdentity } from '@ldclabs/ic-auth'
3- import assert from 'node:assert'
7+ import * as readline from 'node:readline/promises'
8+ import { stdin as input , stdout as output } from 'node:process'
9+ import { readFileSync , writeFileSync } from 'node:fs'
410
11+ // Run with:
512// npx tsx app.ts
613async function main ( ) {
7- const canisterId = 'ogkpr-lyaaa-aaaap-an5fq-cai' // Replace with your canister ID
8- const assetId = 'druyg-tyaaa-aaaaq-aactq-cai' // PANDA token canister ID
9- const identity = Ed25519KeyIdentity . fromSecretKey ( new Uint8Array ( 32 ) . fill ( 8 ) )
10- assert . equal (
11- identity . getPrincipal ( ) . toText ( ) ,
12- 'jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe'
13- )
14+ const rl = readline . createInterface ( { input, output } )
15+
16+ // This is the Anda x402 Facilitator
17+ // https://ogkpr-lyaaa-aaaap-an5fq-cai.icp0.io
18+ const facilitatorCanisterId = 'ogkpr-lyaaa-aaaap-an5fq-cai'
1419
15- const x402 = new X402Canister ( canisterId , identity , 'http://localhost:4943' )
20+ // Load or create identity
21+ let identity : Ed25519KeyIdentity
22+ try {
23+ const identityJson = readFileSync ( 'icp_x402_identity.json' , 'utf-8' )
24+ identity = Ed25519KeyIdentity . fromJSON ( identityJson )
25+ } catch ( _e ) {
26+ identity = Ed25519KeyIdentity . generate ( )
27+ writeFileSync ( 'icp_x402_identity.json' , JSON . stringify ( identity . toJSON ( ) ) )
28+ console . log (
29+ `Generated new identity and saved to icp_x402_identity.json.\nPrincipal: ${ identity
30+ . getPrincipal ( )
31+ . toText ( ) } \n`
32+ )
33+ }
1634
35+ const x402 = new X402Canister ( facilitatorCanisterId , identity )
1736 const info = await x402 . getInfo ( )
18- console . log ( 'Info:' , info )
37+ console . log ( `Welcome to the ${ info . name } !` )
38+ console . log ( `Your wallet principal: ${ identity . getPrincipal ( ) . toText ( ) } ` )
1939
20- const payTo = 'rrkah-fqaaa-aaaaa-aaaaq-cai'
21- const before = await x402 . getBalanceOf ( assetId , payTo )
22- console . log ( 'Balance:' , before )
40+ const assetChoices = Object . entries ( info . supportedAssets || { } )
41+ if ( assetChoices . length === 0 ) {
42+ console . log ( 'No supported assets found.' )
43+ rl . close ( )
44+ return
45+ }
46+
47+ console . log ( '\nPlease select a token to pay with:' )
48+ assetChoices . forEach ( ( [ assetId , assetInfo ] , index ) => {
49+ console . log ( `${ index + 1 } ) ${ assetInfo . symbol } (${ assetId } )` )
50+ } )
2351
52+ const choiceAnswer = await rl . question (
53+ `Enter your choice (1-${ assetChoices . length } ): `
54+ )
55+ const choiceIndex = parseInt ( choiceAnswer , 10 ) - 1
56+ const [ assetId , selectedAssetInfo ] = assetChoices [ choiceIndex ] || { }
57+ if ( ! selectedAssetInfo ) {
58+ console . log ( `Invalid choice ${ choiceAnswer } ` )
59+ rl . close ( )
60+ return
61+ }
62+
63+ const amount = 1n * 10n ** BigInt ( selectedAssetInfo . decimals )
64+ // https://dmsg.net/PANDA wallet
65+ const payTo =
66+ '77ibd-jp5kr-moeco-kgoar-rro5v-5tng4-krif5-5h2i6-osf2f-2sjtv-kqe'
2467 const req : PaymentRequirementsResponse = {
2568 x402Version : 1 ,
2669 error : 'some error' ,
2770 accepts : [
2871 {
2972 scheme : 'exact' ,
3073 network : x402 . network ,
31- maxAmountRequired : '100000000' , // 1 PANDA
74+ maxAmountRequired : amount . toString ( ) ,
3275 asset : assetId ,
3376 payTo,
3477 resource : 'https://github.com/ldclabs' ,
@@ -38,38 +81,62 @@ async function main() {
3881 ]
3982 }
4083
41- // Client: approve allowance
42- await x402 . ensureAllowance ( assetId , BigInt ( 100000000 + 10000 ) ) // 1 PANDA + fee
43-
4484 // Client: build x402 request
4585 const x402Request = await x402 . buildX402Request ( req , assetId )
46- console . log ( '\n\nX402 Request:' , x402Request )
47-
48- // Resource server: verify x402 request
49- const verifyRes = await fetch (
50- `http://127.0.0.1:4943/verify?canisterId=${ canisterId } ` ,
51- {
52- method : 'POST' ,
53- headers : { 'Content-Type' : 'application/json' } ,
54- body : JSON . stringify ( x402Request )
55- }
86+ console . log (
87+ `\nTipping ${ formatAmount ( amount , selectedAssetInfo . decimals ) } ${ selectedAssetInfo . symbol } to https://dmsg.net/PANDA wallet (${ payTo } )`
88+ )
89+ console . log ( '\nX402 Request:' , JSON . stringify ( x402Request , null , 2 ) )
90+
91+ const before = await x402 . getBalanceOf ( assetId , identity . getPrincipal ( ) )
92+ console . log (
93+ `\nYour balance: ${ formatAmount ( before , selectedAssetInfo . decimals ) } ${ selectedAssetInfo . symbol } `
94+ )
95+ // Client: approve allowance
96+ const amountToApprove = amount + BigInt ( selectedAssetInfo . transferFee )
97+ const answer = await rl . question (
98+ `Approve allowance of ${ formatAmount ( amountToApprove , selectedAssetInfo . decimals ) } ${ selectedAssetInfo . symbol } (with ${ formatAmount ( selectedAssetInfo . transferFee , selectedAssetInfo . decimals ) } fee)? (y/N) `
5699 )
57- const verifyJson = await verifyRes . json ( )
58- console . log ( '\n\nVerify Response:' , verifyJson )
100+
101+ if ( before < amountToApprove ) {
102+ console . log (
103+ `Insufficient balance. You need at least ${ formatAmount (
104+ amountToApprove ,
105+ selectedAssetInfo . decimals
106+ ) } ${ selectedAssetInfo . symbol } .`
107+ )
108+ rl . close ( )
109+ return
110+ }
111+
112+ if ( answer . toLowerCase ( ) !== 'y' && answer . toLowerCase ( ) !== 'yes' ) {
113+ console . log ( 'Aborted by user.' )
114+ rl . close ( )
115+ return
116+ }
117+
118+ await x402 . ensureAllowance ( assetId , amountToApprove )
119+ console . log ( 'Allowance approved, starting payment...' )
120+
121+ // Resource server: verify x402 request (optional)
122+ // const verifyRes = await fetch(`${x402.endpoint}/verify`, {
123+ // method: 'POST',
124+ // headers: { 'Content-Type': 'application/json' },
125+ // body: JSON.stringify(x402Request)
126+ // })
127+ // const verifyJson = await verifyRes.json()
128+ // console.log('\n\nVerify Response:', verifyJson)
59129 // Verify Response: {
60130 // isValid: true,
61131 // payer: 'jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe'
62132 // }
63133
64134 // Resource server: settle x402 request
65- const settleRes = await fetch (
66- `http://127.0.0.1:4943/settle?canisterId=${ canisterId } ` ,
67- {
68- method : 'POST' ,
69- headers : { 'Content-Type' : 'application/json' } ,
70- body : JSON . stringify ( x402Request )
71- }
72- )
135+ const settleRes = await fetch ( `${ x402 . endpoint } /settle` , {
136+ method : 'POST' ,
137+ headers : { 'Content-Type' : 'application/json' } ,
138+ body : JSON . stringify ( x402Request )
139+ } )
73140 const settleJson = await settleRes . json ( )
74141 console . log ( '\n\nSettle Response:' , settleJson )
75142 // Settle Response: {
@@ -79,12 +146,20 @@ async function main() {
79146 // payer: 'jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe'
80147 // }
81148
82- const after = await x402 . getBalanceOf ( assetId , payTo )
83- console . log ( 'Balance:' , before , '->' , after )
149+ await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) // wait for fee transaction
150+ const after = await x402 . getBalanceOf ( assetId , identity . getPrincipal ( ) )
151+ console . log (
152+ `Your balance:` ,
153+ formatAmount ( before , selectedAssetInfo . decimals ) ,
154+ '->' ,
155+ formatAmount ( after , selectedAssetInfo . decimals ) ,
156+ selectedAssetInfo . symbol
157+ )
84158
85159 // Client: list my payment logs
86- const logs = await x402 . listMyPaymentLogs ( 3 )
87- console . log ( '\n\nMy Payment Logs:' , logs )
160+ const logs = await x402 . listMyPaymentLogs ( 2 )
161+ console . log ( '\n\nYour Payment Logs:' , logs )
162+ rl . close ( )
88163}
89164
90165main ( ) . catch ( ( error ) => {
0 commit comments