qorbitpay-sdk 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/package.json +45 -0
- package/src/abi.js +55 -0
- package/src/config.js +18 -0
- package/src/index.js +314 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Qorbitpay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# qorbitpay-sdk
|
|
2
|
+
|
|
3
|
+
Payments SDK for autonomous AI agents on [Qorbitpay](https://github.com/), running on Arc
|
|
4
|
+
(Circle's L1 testnet, chainId `5042002`, native gas token USDC).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install qorbitpay-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quickstart
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { Qorbitpay } from 'qorbitpay-sdk';
|
|
14
|
+
|
|
15
|
+
const q = new Qorbitpay({ privateKey: process.env.AGENT_KEY });
|
|
16
|
+
|
|
17
|
+
// send a payment (self-sovereign - signed and broadcast with your own wallet)
|
|
18
|
+
await q.pay({ to: '0xAgent', amount: 0.01, memo: 'service' });
|
|
19
|
+
|
|
20
|
+
// listen for incoming payments
|
|
21
|
+
const unsubscribe = q.receive({
|
|
22
|
+
onPayment: (tx) => fulfillService(tx),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Fund your agent's wallet at the [Circle faucet](https://faucet.circle.com) (select Arc
|
|
27
|
+
Testnet) before sending payments.
|
|
28
|
+
|
|
29
|
+
## Why no custodial relayer here
|
|
30
|
+
|
|
31
|
+
Qorbitpay's hosted backend (`POST /api/pay`) is a *custodial* relayer that adds QRNG-anchored
|
|
32
|
+
nonces, VQC fraud screening, QAOA-based provider routing, and Dilithium2 (post-quantum)
|
|
33
|
+
dual-signing with on-chain attestation for higher-value payments. This SDK instead talks
|
|
34
|
+
directly to the on-chain contracts with your agent's own key - lighter weight, no
|
|
35
|
+
custodial trust required, and all you need for the base protocol (sending/receiving
|
|
36
|
+
native-token payments, registering in the agent directory).
|
|
37
|
+
|
|
38
|
+
`checkFraud()` and `findRoute()` below call the *same* quantum services the hosted backend
|
|
39
|
+
uses, over HTTP, for agents that want those signals without running Qiskit themselves -
|
|
40
|
+
pass `apiUrl` pointing at a Qorbitpay backend deployment to use them.
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### `new Qorbitpay(options)`
|
|
45
|
+
|
|
46
|
+
| option | required | default | description |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| `privateKey` | yes | - | your agent's wallet private key |
|
|
49
|
+
| `rpcUrl` | no | Arc testnet RPC | |
|
|
50
|
+
| `chainId` | no | `5042002` | |
|
|
51
|
+
| `routerAddress` | no | deployed QorbitpayRouter | |
|
|
52
|
+
| `registryAddress` | no | deployed QorbitpayRegistry | |
|
|
53
|
+
| `apiUrl` | no | `null` | base URL of a Qorbitpay backend, required for `checkFraud`/`findRoute` |
|
|
54
|
+
|
|
55
|
+
### `await q.pay({ to, amount, memo })`
|
|
56
|
+
Sends `amount` (native token units, string or number) to `to`. `memo` is hashed
|
|
57
|
+
(keccak256) on-chain. Returns `{ txHash, status, blockNumber, explorerUrl, ... }`.
|
|
58
|
+
|
|
59
|
+
### `q.receive({ onPayment })`
|
|
60
|
+
Subscribes to incoming `PaymentSent` events addressed to your agent. Returns an
|
|
61
|
+
`unsubscribe()` function.
|
|
62
|
+
|
|
63
|
+
### `await q.register({ serviceEndpoint })`
|
|
64
|
+
Registers your agent (self-sovereign: owner == agent == your wallet) in the on-chain
|
|
65
|
+
agent directory.
|
|
66
|
+
|
|
67
|
+
### `await q.getAgent(address?)` / `await q.getReputation(address?)`
|
|
68
|
+
Reads from the agent directory. Defaults to your own address.
|
|
69
|
+
|
|
70
|
+
### `await q.getBalance()`
|
|
71
|
+
Native token balance of your agent's wallet, formatted as a string.
|
|
72
|
+
|
|
73
|
+
### `await q.checkFraud({ amount_zscore, agent_age_days, tx_frequency, dispute_rate })`
|
|
74
|
+
VQC (variational quantum circuit) fraud score via a Qorbitpay backend. Requires `apiUrl`.
|
|
75
|
+
|
|
76
|
+
### `await q.findRoute(providers)`
|
|
77
|
+
QAOA-optimal pick among `providers: [{ id, price, latency, reputation }, ...]` via a
|
|
78
|
+
Qorbitpay backend. Requires `apiUrl`.
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qorbitpay-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK for autonomous AI agents to send and receive payments on Qorbitpay (Arc testnet)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"qorbitpay",
|
|
20
|
+
"arc",
|
|
21
|
+
"circle",
|
|
22
|
+
"usdc",
|
|
23
|
+
"ai-agents",
|
|
24
|
+
"x402",
|
|
25
|
+
"payments",
|
|
26
|
+
"web3"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": "Qorbitpay",
|
|
30
|
+
"homepage": "https://qorbitpay.xyz",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/davieslennox0/Qorbit.git",
|
|
34
|
+
"directory": "sdk"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/davieslennox0/Qorbit/issues"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"ethers": "^6.13.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"ethers": "^6.13.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/abi.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const ROUTER_ABI = [
|
|
2
|
+
'function pay(address to, bytes32 memoHash) payable',
|
|
3
|
+
'function splitPay(address[] to, uint256[] amounts, bytes32 memoHash) payable',
|
|
4
|
+
'function createEscrow(address payee, uint64 timeoutSeconds) payable returns (uint256)',
|
|
5
|
+
'function releaseEscrow(uint256 id)',
|
|
6
|
+
'function refundEscrow(uint256 id)',
|
|
7
|
+
'function getEscrow(uint256 id) view returns (tuple(address payer,address payee,uint256 amount,uint64 createdAt,uint64 timeoutSeconds,bool released,bool refunded))',
|
|
8
|
+
'event PaymentSent(address indexed from, address indexed to, uint256 amount, bytes32 memoHash)',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const _IDENTITY = 'tuple(address owner,string name,string version,string serviceEndpoint,bytes32 capabilityHash,uint256 registeredAt,bool active)';
|
|
12
|
+
const _RECORD = 'tuple(address owner,string name,string version,string serviceEndpoint,bytes32 capabilityHash,uint256 registeredAt,bool active,uint32 reputation,bytes32 dilithiumPubKeyHash,uint32 trustScore,uint8 quantumLayerMask)';
|
|
13
|
+
|
|
14
|
+
export const REGISTRY_ABI = [
|
|
15
|
+
// ERC-8004 required
|
|
16
|
+
`function registerAgent(${_IDENTITY} identity) returns (bytes32 agentId)`,
|
|
17
|
+
`function getAgent(bytes32 agentId) view returns (${_IDENTITY})`,
|
|
18
|
+
'function validateAgent(bytes32 agentId) view returns (bool)',
|
|
19
|
+
'function deactivateAgent(bytes32 agentId)',
|
|
20
|
+
'event AgentRegistered(bytes32 indexed agentId, address indexed owner, string name)',
|
|
21
|
+
'event AgentValidated(bytes32 indexed agentId)',
|
|
22
|
+
'event AgentDeactivated(bytes32 indexed agentId)',
|
|
23
|
+
// Qorbitpay extensions
|
|
24
|
+
`function getAgentRecord(bytes32 agentId) view returns (${_RECORD})`,
|
|
25
|
+
`function getAgentByOwner(address owner) view returns (bytes32 agentId, ${_RECORD} record)`,
|
|
26
|
+
'function updateEndpoint(bytes32 agentId, string serviceEndpoint)',
|
|
27
|
+
'function setDilithiumPubKey(bytes32 agentId, bytes32 pubKeyHash)',
|
|
28
|
+
'function isRegistered(address agentOwner) view returns (bool)',
|
|
29
|
+
'function ownerToAgentId(address) view returns (bytes32)',
|
|
30
|
+
'function agentCount() view returns (uint256)',
|
|
31
|
+
`function listAgents(uint256 offset, uint256 limit) view returns (${_RECORD}[] page, bytes32[] ids)`,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const BILLING_ABI = [
|
|
35
|
+
'function createSubscription(address agent, uint256 amountPerPeriod, uint256 periodSeconds) payable returns (bytes32)',
|
|
36
|
+
'function cancelSubscription(bytes32 subId)',
|
|
37
|
+
'function processPayment(bytes32 subId)',
|
|
38
|
+
'function topUp(bytes32 subId) payable',
|
|
39
|
+
'function getSubscription(bytes32 subId) view returns (tuple(address subscriber,address agent,uint256 amountPerPeriod,uint256 periodSeconds,uint256 lastPaidAt,uint256 balance,bool active))',
|
|
40
|
+
'function nextPaymentDue(bytes32 subId) view returns (uint256)',
|
|
41
|
+
'function isDue(bytes32 subId) view returns (bool)',
|
|
42
|
+
'event SubscriptionCreated(bytes32 indexed subId, address indexed subscriber, address indexed agent, uint256 amountPerPeriod, uint256 periodSeconds)',
|
|
43
|
+
'event PaymentProcessed(bytes32 indexed subId, address indexed agent, uint256 amount)',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const TREASURY_ABI = [
|
|
47
|
+
'function deposit() payable',
|
|
48
|
+
'function allocateBudget(address workerAgent, uint256 dailyCap, uint256 categoryMask, uint256 expiresAt)',
|
|
49
|
+
'function spend(address treasury, uint256 amount, uint256 category)',
|
|
50
|
+
'function revokeBudget(address workerAgent)',
|
|
51
|
+
'function getBudget(address treasury, address workerAgent) view returns (uint256 remaining, uint256 dailyCap, uint256 expiresAt, uint256 categoryMask, bool active)',
|
|
52
|
+
'function treasuryBalances(address) view returns (uint256)',
|
|
53
|
+
'event BudgetAllocated(address indexed treasury, address indexed worker, uint256 dailyCap, uint256 categoryMask, uint256 expiresAt)',
|
|
54
|
+
'event SpendExecuted(address indexed treasury, address indexed worker, uint256 amount, uint256 category)',
|
|
55
|
+
];
|
package/src/config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Defaults for Arc testnet - see contracts/deployments/arc-testnet.json in the Qorbitpay repo.
|
|
2
|
+
export const DEFAULT_NETWORK = {
|
|
3
|
+
rpcUrl: 'https://rpc.testnet.arc.network',
|
|
4
|
+
chainId: 5042002,
|
|
5
|
+
explorer: 'https://testnet.arcscan.app',
|
|
6
|
+
routerAddress: '0xCEe1f311261Ffe460ef5060F94183320e74fD703',
|
|
7
|
+
registryAddress: '0x3fbCdaD38f40d932A5574562BB72B9115c265093',
|
|
8
|
+
verifierAddress: '0xca8FbCFb990A77B130939Ec5E0B98e4324fE0c79',
|
|
9
|
+
billingAddress: '0xdF87E0c0cfcA0AEa1e899073c36F29Fd865B5e97',
|
|
10
|
+
treasuryAddress: '0x7168493186654AD84Ef2dEfb78C773eeCB1BaFC8',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const CATEGORY = {
|
|
14
|
+
data: 0x01,
|
|
15
|
+
compute: 0x02,
|
|
16
|
+
storage: 0x04,
|
|
17
|
+
all: 0x07,
|
|
18
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { ROUTER_ABI, REGISTRY_ABI, BILLING_ABI, TREASURY_ABI } from './abi.js';
|
|
3
|
+
import { DEFAULT_NETWORK, CATEGORY } from './config.js';
|
|
4
|
+
|
|
5
|
+
/// Qorbitpay SDK - a thin client for AI agents to send/receive payments directly on Arc,
|
|
6
|
+
/// using the agent's own funded wallet (self-sovereign), rather than going through
|
|
7
|
+
/// Qorbitpay's custodial backend relayer. The relayer's extra protections (QRNG-anchored
|
|
8
|
+
/// nonces, VQC fraud screening, QAOA routing, Dilithium dual-signing/attestation) are
|
|
9
|
+
/// features of the hosted POST /api/pay endpoint, not requirements of the on-chain
|
|
10
|
+
/// protocol itself - calling the contracts directly here is a fully valid, lighter-weight
|
|
11
|
+
/// way to use Qorbitpay. checkFraud()/findRoute() below call the hosted quantum services
|
|
12
|
+
/// over HTTP for agents that want those signals without running Qiskit themselves.
|
|
13
|
+
class Qorbitpay {
|
|
14
|
+
constructor({
|
|
15
|
+
privateKey,
|
|
16
|
+
rpcUrl = DEFAULT_NETWORK.rpcUrl,
|
|
17
|
+
chainId = DEFAULT_NETWORK.chainId,
|
|
18
|
+
routerAddress = DEFAULT_NETWORK.routerAddress,
|
|
19
|
+
registryAddress = DEFAULT_NETWORK.registryAddress,
|
|
20
|
+
billingAddress = DEFAULT_NETWORK.billingAddress,
|
|
21
|
+
treasuryAddress = DEFAULT_NETWORK.treasuryAddress,
|
|
22
|
+
apiUrl = null,
|
|
23
|
+
} = {}) {
|
|
24
|
+
if (!privateKey) throw new Error('Qorbitpay SDK requires a privateKey');
|
|
25
|
+
|
|
26
|
+
this.provider = new ethers.JsonRpcProvider(rpcUrl, { chainId, name: 'arc-testnet' });
|
|
27
|
+
this.wallet = new ethers.Wallet(privateKey, this.provider);
|
|
28
|
+
this.address = this.wallet.address;
|
|
29
|
+
this.apiUrl = apiUrl;
|
|
30
|
+
this.explorer = DEFAULT_NETWORK.explorer;
|
|
31
|
+
|
|
32
|
+
this.router = new ethers.Contract(routerAddress, ROUTER_ABI, this.wallet);
|
|
33
|
+
this.registry = new ethers.Contract(registryAddress, REGISTRY_ABI, this.wallet);
|
|
34
|
+
this.billing = new ethers.Contract(billingAddress, BILLING_ABI, this.wallet);
|
|
35
|
+
this.treasury = new ethers.Contract(treasuryAddress, TREASURY_ABI, this.wallet);
|
|
36
|
+
|
|
37
|
+
// ERC-8004 agentId, populated after register() or set manually.
|
|
38
|
+
this.agentId = null;
|
|
39
|
+
|
|
40
|
+
// Guards against nonce races if an agent fires multiple calls without awaiting each one.
|
|
41
|
+
this._sendQueue = Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_withNonceLock(fn) {
|
|
45
|
+
const run = this._sendQueue.then(async () => {
|
|
46
|
+
const nonce = await this.provider.getTransactionCount(this.address, 'pending');
|
|
47
|
+
return fn(nonce);
|
|
48
|
+
});
|
|
49
|
+
this._sendQueue = run.then(() => {}, () => {});
|
|
50
|
+
return run;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Sends a native-token (USDC) payment to `to` via QorbitpayRouter.pay(). `memo` is
|
|
54
|
+
/// hashed (keccak256) on-chain as an opaque reference - pass the same memo string
|
|
55
|
+
/// elsewhere if you need to look the payment back up.
|
|
56
|
+
async pay({ to, amount, memo } = {}) {
|
|
57
|
+
if (!to || !ethers.isAddress(to)) throw new Error('pay() requires a valid `to` address');
|
|
58
|
+
if (!amount || Number(amount) <= 0) throw new Error('pay() requires a positive `amount`');
|
|
59
|
+
|
|
60
|
+
const memoHash = memo ? ethers.keccak256(ethers.toUtf8Bytes(memo)) : ethers.ZeroHash;
|
|
61
|
+
const value = ethers.parseEther(String(amount));
|
|
62
|
+
|
|
63
|
+
const tx = await this._withNonceLock((nonce) => this.router.pay(to, memoHash, { value, nonce }));
|
|
64
|
+
const receipt = await tx.wait();
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
txHash: tx.hash,
|
|
68
|
+
from: this.address,
|
|
69
|
+
to,
|
|
70
|
+
amount: String(amount),
|
|
71
|
+
memo: memo || null,
|
|
72
|
+
status: receipt.status === 1 ? 'confirmed' : 'failed',
|
|
73
|
+
blockNumber: receipt.blockNumber,
|
|
74
|
+
explorerUrl: `${this.explorer}/tx/${tx.hash}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Subscribes to incoming payments (PaymentSent events where `to` is this agent's
|
|
79
|
+
/// address). Returns an unsubscribe function.
|
|
80
|
+
receive({ onPayment } = {}) {
|
|
81
|
+
if (typeof onPayment !== 'function') throw new Error('receive() requires an onPayment callback');
|
|
82
|
+
|
|
83
|
+
const filter = this.router.filters.PaymentSent(null, this.address);
|
|
84
|
+
// Subscribing with a TopicFilter object (rather than the bare event name string)
|
|
85
|
+
// means ethers does NOT expand decoded args into separate listener parameters - it
|
|
86
|
+
// passes a single ContractEventPayload. Assuming the (from, to, amount, memoHash,
|
|
87
|
+
// event) signature here silently threw inside the listener (event was undefined) and
|
|
88
|
+
// ethers swallowed it, which looked exactly like a polling/timing bug until logging
|
|
89
|
+
// the raw listener arguments showed only one object was ever passed.
|
|
90
|
+
const listener = (payload) => {
|
|
91
|
+
const [from, to, amount, memoHash] = payload.args;
|
|
92
|
+
onPayment({
|
|
93
|
+
from,
|
|
94
|
+
to,
|
|
95
|
+
amount: ethers.formatEther(amount),
|
|
96
|
+
memoHash,
|
|
97
|
+
txHash: payload.log.transactionHash,
|
|
98
|
+
blockNumber: payload.log.blockNumber,
|
|
99
|
+
explorerUrl: `${this.explorer}/tx/${payload.log.transactionHash}`,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
this.router.on(filter, listener);
|
|
104
|
+
return () => this.router.off(filter, listener);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Registers this agent in QorbitpayRegistry using an ERC-8004 AgentIdentity struct
|
|
108
|
+
/// (self-sovereign — owner is this wallet). Returns the bytes32 agentId and stores it
|
|
109
|
+
/// on this instance for automatic inclusion as X-Agent-Id in all subsequent API calls.
|
|
110
|
+
/// For custodial registration on an agent's behalf, see the backend's POST /api/receive.
|
|
111
|
+
async register({ name, serviceEndpoint, version = '1.0.0', capabilityHash } = {}) {
|
|
112
|
+
if (!name) throw new Error('register() requires a name');
|
|
113
|
+
if (!serviceEndpoint) throw new Error('register() requires a serviceEndpoint');
|
|
114
|
+
|
|
115
|
+
const identity = {
|
|
116
|
+
owner: this.address,
|
|
117
|
+
name,
|
|
118
|
+
version,
|
|
119
|
+
serviceEndpoint,
|
|
120
|
+
capabilityHash: capabilityHash || ethers.ZeroHash,
|
|
121
|
+
registeredAt: 0n,
|
|
122
|
+
active: true,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const tx = await this._withNonceLock((nonce) =>
|
|
126
|
+
this.registry.registerAgent(identity, { nonce })
|
|
127
|
+
);
|
|
128
|
+
const receipt = await tx.wait();
|
|
129
|
+
|
|
130
|
+
let agentId = null;
|
|
131
|
+
for (const log of receipt.logs) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = this.registry.interface.parseLog({ topics: log.topics, data: log.data });
|
|
134
|
+
if (parsed?.name === 'AgentRegistered') { agentId = parsed.args.agentId; break; }
|
|
135
|
+
} catch { /* ignore */ }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.agentId = agentId;
|
|
139
|
+
return { txHash: tx.hash, agentId, status: receipt.status === 1 ? 'confirmed' : 'failed' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_formatAgentRecord(agentId, record) {
|
|
143
|
+
return {
|
|
144
|
+
agentId,
|
|
145
|
+
erc8004: true,
|
|
146
|
+
owner: record.owner,
|
|
147
|
+
name: record.name,
|
|
148
|
+
version: record.version,
|
|
149
|
+
serviceEndpoint: record.serviceEndpoint,
|
|
150
|
+
capabilityHash: record.capabilityHash,
|
|
151
|
+
registeredAt: Number(record.registeredAt),
|
|
152
|
+
active: record.active,
|
|
153
|
+
reputation: Number(record.reputation),
|
|
154
|
+
dilithiumPubKeyHash: record.dilithiumPubKeyHash,
|
|
155
|
+
trustScore: Number(record.trustScore),
|
|
156
|
+
quantumLayerMask: Number(record.quantumLayerMask),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getAgent(agentIdOrAddress = this.agentId || this.address) {
|
|
161
|
+
// Accept either a bytes32 agentId (66-char hex) or an owner address (42-char).
|
|
162
|
+
if (agentIdOrAddress && agentIdOrAddress.length > 42) {
|
|
163
|
+
const record = await this.registry.getAgentRecord(agentIdOrAddress);
|
|
164
|
+
return this._formatAgentRecord(agentIdOrAddress, record);
|
|
165
|
+
}
|
|
166
|
+
const [agentId, record] = await this.registry.getAgentByOwner(agentIdOrAddress);
|
|
167
|
+
return this._formatAgentRecord(agentId, record);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async getReputation(agentIdOrAddress = this.agentId || this.address) {
|
|
171
|
+
return (await this.getAgent(agentIdOrAddress)).reputation;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getBalance() {
|
|
175
|
+
const balance = await this.provider.getBalance(this.address);
|
|
176
|
+
return ethers.formatEther(balance);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async _apiPost(path, body) {
|
|
180
|
+
if (!this.apiUrl) {
|
|
181
|
+
throw new Error(`${path} requires apiUrl to be set in the Qorbitpay constructor`);
|
|
182
|
+
}
|
|
183
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
184
|
+
if (this.agentId) headers['X-Agent-Id'] = this.agentId;
|
|
185
|
+
const res = await fetch(`${this.apiUrl}${path}`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers,
|
|
188
|
+
body: JSON.stringify(body),
|
|
189
|
+
});
|
|
190
|
+
const data = await res.json();
|
|
191
|
+
if (!res.ok) throw new Error(data.error || `${path} failed with status ${res.status}`);
|
|
192
|
+
return data;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// VQC fraud score via the hosted quantum services (POST /api/fraud).
|
|
196
|
+
checkFraud({ amount_zscore, agent_age_days, tx_frequency, dispute_rate } = {}) {
|
|
197
|
+
return this._apiPost('/api/fraud', { amount_zscore, agent_age_days, tx_frequency, dispute_rate });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// QAOA-optimal provider selection via the hosted quantum services (POST /api/route).
|
|
201
|
+
findRoute(providers) {
|
|
202
|
+
return this._apiPost('/api/route', { providers });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Create a recurring subscription to pay `to` agent `amount` USDC every `period`.
|
|
206
|
+
/// `period` can be seconds (number) or a shorthand string: '7d', '24h', '1w', etc.
|
|
207
|
+
/// `initialBalance` (optional) pre-funds the subscription; defaults to one period's amount.
|
|
208
|
+
async subscribe({ to, amount, period, initialBalance } = {}) {
|
|
209
|
+
if (!to || !ethers.isAddress(to)) throw new Error('subscribe() requires a valid `to` address');
|
|
210
|
+
if (!amount || Number(amount) <= 0) throw new Error('subscribe() requires a positive `amount`');
|
|
211
|
+
if (!period) throw new Error('subscribe() requires a `period` (e.g. "7d", 604800)');
|
|
212
|
+
|
|
213
|
+
const periodSeconds = BigInt(_parsePeriod(period));
|
|
214
|
+
const amountWei = ethers.parseEther(String(amount));
|
|
215
|
+
const balanceWei = initialBalance ? ethers.parseEther(String(initialBalance)) : amountWei;
|
|
216
|
+
|
|
217
|
+
const tx = await this._withNonceLock((nonce) =>
|
|
218
|
+
this.billing.createSubscription(to, amountWei, periodSeconds, { value: balanceWei, nonce })
|
|
219
|
+
);
|
|
220
|
+
const receipt = await tx.wait();
|
|
221
|
+
|
|
222
|
+
let subId = null;
|
|
223
|
+
for (const log of receipt.logs) {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = this.billing.interface.parseLog({ topics: log.topics, data: log.data });
|
|
226
|
+
if (parsed?.name === 'SubscriptionCreated') {
|
|
227
|
+
subId = parsed.args.subId;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
} catch { /* ignore */ }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
subId,
|
|
235
|
+
to,
|
|
236
|
+
amount: String(amount),
|
|
237
|
+
periodSeconds: Number(periodSeconds),
|
|
238
|
+
initialBalance: ethers.formatEther(balanceWei),
|
|
239
|
+
txHash: tx.hash,
|
|
240
|
+
status: receipt.status === 1 ? 'confirmed' : 'failed',
|
|
241
|
+
explorerUrl: `${this.explorer}/tx/${tx.hash}`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// Cancel a subscription (subscriber only). Refunds remaining balance.
|
|
246
|
+
async cancelSubscription(subId) {
|
|
247
|
+
if (!subId) throw new Error('cancelSubscription() requires a subId');
|
|
248
|
+
const tx = await this._withNonceLock((nonce) => this.billing.cancelSubscription(subId, { nonce }));
|
|
249
|
+
const receipt = await tx.wait();
|
|
250
|
+
return { txHash: tx.hash, status: receipt.status === 1 ? 'confirmed' : 'failed' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Spend from a treasury budget. `from` is the treasury address, `category` is
|
|
254
|
+
/// 'data' | 'compute' | 'storage' or a numeric bitmask.
|
|
255
|
+
async spend({ from, amount, category = 'data' } = {}) {
|
|
256
|
+
if (!from || !ethers.isAddress(from)) throw new Error('spend() requires a valid `from` (treasury) address');
|
|
257
|
+
if (!amount || Number(amount) <= 0) throw new Error('spend() requires a positive `amount`');
|
|
258
|
+
|
|
259
|
+
const cat = typeof category === 'string'
|
|
260
|
+
? (CATEGORY[category.toLowerCase()] ?? (() => { throw new Error(`unknown category: ${category}`); })())
|
|
261
|
+
: Number(category);
|
|
262
|
+
const amountWei = ethers.parseEther(String(amount));
|
|
263
|
+
|
|
264
|
+
const tx = await this._withNonceLock((nonce) =>
|
|
265
|
+
this.treasury.spend(from, amountWei, BigInt(cat), { nonce })
|
|
266
|
+
);
|
|
267
|
+
const receipt = await tx.wait();
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
from,
|
|
271
|
+
to: this.address,
|
|
272
|
+
amount: String(amount),
|
|
273
|
+
category,
|
|
274
|
+
txHash: tx.hash,
|
|
275
|
+
status: receipt.status === 1 ? 'confirmed' : 'failed',
|
|
276
|
+
explorerUrl: `${this.explorer}/tx/${tx.hash}`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// Deposit funds into the treasury under this agent's address.
|
|
281
|
+
async depositToTreasury(amount) {
|
|
282
|
+
if (!amount || Number(amount) <= 0) throw new Error('depositToTreasury() requires a positive amount');
|
|
283
|
+
const value = ethers.parseEther(String(amount));
|
|
284
|
+
const tx = await this._withNonceLock((nonce) => this.treasury.deposit({ value, nonce }));
|
|
285
|
+
const receipt = await tx.wait();
|
|
286
|
+
return { txHash: tx.hash, status: receipt.status === 1 ? 'confirmed' : 'failed' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Allocate a spending budget to a worker agent.
|
|
290
|
+
async allocateBudget({ worker, dailyCap, categoryMask = CATEGORY.all, expiresAt } = {}) {
|
|
291
|
+
if (!worker || !ethers.isAddress(worker)) throw new Error('allocateBudget() requires a valid `worker` address');
|
|
292
|
+
if (!dailyCap || Number(dailyCap) <= 0) throw new Error('allocateBudget() requires a positive `dailyCap`');
|
|
293
|
+
if (!expiresAt) throw new Error('allocateBudget() requires an `expiresAt` unix timestamp');
|
|
294
|
+
|
|
295
|
+
const tx = await this._withNonceLock((nonce) =>
|
|
296
|
+
this.treasury.allocateBudget(worker, ethers.parseEther(String(dailyCap)), BigInt(categoryMask), BigInt(expiresAt), { nonce })
|
|
297
|
+
);
|
|
298
|
+
const receipt = await tx.wait();
|
|
299
|
+
return { txHash: tx.hash, status: receipt.status === 1 ? 'confirmed' : 'failed' };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _parsePeriod(period) {
|
|
304
|
+
if (typeof period === 'number') return period;
|
|
305
|
+
const str = String(period);
|
|
306
|
+
if (/^\d+$/.test(str)) return Number(str);
|
|
307
|
+
const match = str.match(/^(\d+(?:\.\d+)?)(s|m|h|d|w)$/);
|
|
308
|
+
if (!match) throw new Error(`invalid period: ${period} (use e.g. 7d, 24h, 3600)`);
|
|
309
|
+
const n = Number(match[1]);
|
|
310
|
+
const units = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };
|
|
311
|
+
return Math.round(n * units[match[2]]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export { Qorbitpay, CATEGORY };
|