Skip to content

Commit 7119153

Browse files
committed
chore: 1. improve asset info; 2. add example with prod
1 parent 36a4e4f commit 7119153

File tree

22 files changed

+290
-57
lines changed

22 files changed

+290
-57
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ node_modules/
2323
.vscode
2424

2525
# Cargo.lock
26-
proposal-message.json
26+
proposal-message.json
27+
**/*identity.json

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ strip = true
1313
opt-level = 's'
1414

1515
[workspace.package]
16-
version = "0.3.2"
16+
version = "0.3.3"
1717
edition = "2024"
1818
repository = "https://github.com/ldclabs/anda-cloud"
1919
keywords = ["anda", "agent", "icp", "cloud"]

declarations/anda_x402_canister/anda_x402_canister.did

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
type AssetInfo = record {
22
decimals : nat8;
33
transfer_fee : nat;
4+
logo : opt text;
5+
name : text;
46
payment_fee : nat;
57
symbol : text;
68
};

declarations/anda_x402_canister/anda_x402_canister.did.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { IDL } from '@dfinity/candid';
55
export interface AssetInfo {
66
'decimals' : number,
77
'transfer_fee' : bigint,
8+
'logo' : [] | [string],
9+
'name' : string,
810
'payment_fee' : bigint,
911
'symbol' : string,
1012
}

declarations/anda_x402_canister/anda_x402_canister.did.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const idlFactory = ({ IDL }) => {
2323
const AssetInfo = IDL.Record({
2424
'decimals' : IDL.Nat8,
2525
'transfer_fee' : IDL.Nat,
26+
'logo' : IDL.Opt(IDL.Text),
27+
'name' : IDL.Text,
2628
'payment_fee' : IDL.Nat,
2729
'symbol' : IDL.Text,
2830
});

declarations/anda_x402_canister/anda_x402_canister.mo

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ module {
55
public type AssetInfo = {
66
decimals : Nat8;
77
transfer_fee : Nat;
8+
logo : ?Text;
9+
name : Text;
810
payment_fee : Nat;
911
symbol : Text;
1012
};

examples/ts/anda_x402/app.ts

Lines changed: 117 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,77 @@
1-
import { X402Canister, PaymentRequirementsResponse } from '@ldclabs/anda_x402'
1+
import {
2+
X402Canister,
3+
PaymentRequirementsResponse,
4+
formatAmount
5+
} from '@ldclabs/anda_x402'
26
import { 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
613
async 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

90165
main().catch((error) => {

examples/ts/anda_x402/dev.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { X402Canister, PaymentRequirementsResponse } from '@ldclabs/anda_x402'
2+
import { Ed25519KeyIdentity } from '@ldclabs/ic-auth'
3+
import assert from 'node:assert'
4+
5+
// npx tsx dev.ts
6+
async 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+
15+
const x402 = new X402Canister(canisterId, identity, 'http://localhost:4943')
16+
17+
const info = await x402.getInfo()
18+
console.log('Info:', info)
19+
20+
const payTo = 'rrkah-fqaaa-aaaaa-aaaaq-cai'
21+
const before = await x402.getBalanceOf(assetId, payTo)
22+
console.log('Balance:', before)
23+
24+
const req: PaymentRequirementsResponse = {
25+
x402Version: 1,
26+
error: 'some error',
27+
accepts: [
28+
{
29+
scheme: 'exact',
30+
network: x402.network,
31+
maxAmountRequired: '100000000', // 1 PANDA
32+
asset: assetId,
33+
payTo,
34+
resource: 'https://github.com/ldclabs',
35+
description: 'Payment for some resource',
36+
maxTimeoutSeconds: 300
37+
}
38+
]
39+
}
40+
41+
// Client: approve allowance
42+
await x402.ensureAllowance(assetId, BigInt(100000000 + 10000)) // 1 PANDA + fee
43+
44+
// Client: build x402 request
45+
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+
}
56+
)
57+
const verifyJson = await verifyRes.json()
58+
console.log('\n\nVerify Response:', verifyJson)
59+
// Verify Response: {
60+
// isValid: true,
61+
// payer: 'jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe'
62+
// }
63+
64+
// 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+
)
73+
const settleJson = await settleRes.json()
74+
console.log('\n\nSettle Response:', settleJson)
75+
// Settle Response: {
76+
// success: true,
77+
// transaction: '3:druyg-tyaaa-aaaaq-aactq-cai:30',
78+
// network: 'icp-ogkpr-lyaaa-aaaap-an5fq-cai',
79+
// payer: 'jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe'
80+
// }
81+
82+
const after = await x402.getBalanceOf(assetId, payTo)
83+
console.log('Balance:', before, '->', after)
84+
85+
// Client: list my payment logs
86+
const logs = await x402.listMyPaymentLogs(3)
87+
console.log('\n\nMy Payment Logs:', logs)
88+
}
89+
90+
main().catch((error) => {
91+
console.error('Error in main:', error)
92+
})

0 commit comments

Comments
 (0)