@agirails/sdk 2.0.0-beta
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 +183 -0
- package/dist/ACTPClient.d.ts +52 -0
- package/dist/ACTPClient.d.ts.map +1 -0
- package/dist/ACTPClient.js +120 -0
- package/dist/ACTPClient.js.map +1 -0
- package/dist/abi/ACTPKernel.json +1340 -0
- package/dist/abi/ERC20.json +38 -0
- package/dist/abi/EscrowVault.json +64 -0
- package/dist/builders/DeliveryProofBuilder.d.ts +37 -0
- package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -0
- package/dist/builders/DeliveryProofBuilder.js +165 -0
- package/dist/builders/DeliveryProofBuilder.js.map +1 -0
- package/dist/builders/QuoteBuilder.d.ts +68 -0
- package/dist/builders/QuoteBuilder.d.ts.map +1 -0
- package/dist/builders/QuoteBuilder.js +255 -0
- package/dist/builders/QuoteBuilder.js.map +1 -0
- package/dist/builders/index.d.ts +3 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +10 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/config/networks.d.ts +27 -0
- package/dist/config/networks.d.ts.map +1 -0
- package/dist/config/networks.js +103 -0
- package/dist/config/networks.js.map +1 -0
- package/dist/errors/index.d.ts +38 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +87 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/ACTPKernel.d.ts +30 -0
- package/dist/protocol/ACTPKernel.d.ts.map +1 -0
- package/dist/protocol/ACTPKernel.js +261 -0
- package/dist/protocol/ACTPKernel.js.map +1 -0
- package/dist/protocol/EASHelper.d.ts +23 -0
- package/dist/protocol/EASHelper.d.ts.map +1 -0
- package/dist/protocol/EASHelper.js +106 -0
- package/dist/protocol/EASHelper.js.map +1 -0
- package/dist/protocol/EscrowVault.d.ts +24 -0
- package/dist/protocol/EscrowVault.d.ts.map +1 -0
- package/dist/protocol/EscrowVault.js +114 -0
- package/dist/protocol/EscrowVault.js.map +1 -0
- package/dist/protocol/EventMonitor.d.ts +18 -0
- package/dist/protocol/EventMonitor.d.ts.map +1 -0
- package/dist/protocol/EventMonitor.js +92 -0
- package/dist/protocol/EventMonitor.js.map +1 -0
- package/dist/protocol/MessageSigner.d.ts +23 -0
- package/dist/protocol/MessageSigner.d.ts.map +1 -0
- package/dist/protocol/MessageSigner.js +178 -0
- package/dist/protocol/MessageSigner.js.map +1 -0
- package/dist/protocol/ProofGenerator.d.ts +22 -0
- package/dist/protocol/ProofGenerator.d.ts.map +1 -0
- package/dist/protocol/ProofGenerator.js +64 -0
- package/dist/protocol/ProofGenerator.js.map +1 -0
- package/dist/protocol/QuoteBuilder.d.ts +2 -0
- package/dist/protocol/QuoteBuilder.d.ts.map +1 -0
- package/dist/protocol/QuoteBuilder.js +7 -0
- package/dist/protocol/QuoteBuilder.js.map +1 -0
- package/dist/types/eip712.d.ts +106 -0
- package/dist/types/eip712.d.ts.map +1 -0
- package/dist/types/eip712.js +84 -0
- package/dist/types/eip712.js.map +1 -0
- package/dist/types/escrow.d.ts +18 -0
- package/dist/types/escrow.d.ts.map +1 -0
- package/dist/types/escrow.js +3 -0
- package/dist/types/escrow.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message.d.ts +109 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +3 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/state.d.ts +19 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +49 -0
- package/dist/types/state.js.map +1 -0
- package/dist/types/transaction.d.ts +36 -0
- package/dist/types/transaction.d.ts.map +1 -0
- package/dist/types/transaction.js +3 -0
- package/dist/types/transaction.js.map +1 -0
- package/dist/utils/IPFSClient.d.ts +37 -0
- package/dist/utils/IPFSClient.d.ts.map +1 -0
- package/dist/utils/IPFSClient.js +128 -0
- package/dist/utils/IPFSClient.js.map +1 -0
- package/dist/utils/NonceManager.d.ts +34 -0
- package/dist/utils/NonceManager.d.ts.map +1 -0
- package/dist/utils/NonceManager.js +114 -0
- package/dist/utils/NonceManager.js.map +1 -0
- package/dist/utils/ReceivedNonceTracker.d.ts +35 -0
- package/dist/utils/ReceivedNonceTracker.d.ts.map +1 -0
- package/dist/utils/ReceivedNonceTracker.js +196 -0
- package/dist/utils/ReceivedNonceTracker.js.map +1 -0
- package/dist/utils/canonicalJson.d.ts +4 -0
- package/dist/utils/canonicalJson.d.ts.map +1 -0
- package/dist/utils/canonicalJson.js +21 -0
- package/dist/utils/canonicalJson.js.map +1 -0
- package/dist/utils/computeTypeHash.d.ts +3 -0
- package/dist/utils/computeTypeHash.d.ts.map +1 -0
- package/dist/utils/computeTypeHash.js +30 -0
- package/dist/utils/computeTypeHash.js.map +1 -0
- package/dist/utils/validation.d.ts +6 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +46 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +73 -0
- package/src/ACTPClient.ts +276 -0
- package/src/__tests__/ProofGenerator.test.ts +124 -0
- package/src/__tests__/QuoteBuilder.test.ts +516 -0
- package/src/__tests__/StateMachine.test.ts +82 -0
- package/src/__tests__/builders/DeliveryProofBuilder.test.ts +581 -0
- package/src/__tests__/integration/ACTPClient.test.ts +263 -0
- package/src/__tests__/integration.test.ts +289 -0
- package/src/__tests__/protocol/EASHelper.test.ts +472 -0
- package/src/__tests__/protocol/EventMonitor.test.ts +382 -0
- package/src/__tests__/security/ACTPKernel.security.test.ts +1167 -0
- package/src/__tests__/security/EscrowVault.security.test.ts +570 -0
- package/src/__tests__/security/MessageSigner.security.test.ts +286 -0
- package/src/__tests__/security/NonceReplay.security.test.ts +501 -0
- package/src/__tests__/security/validation.security.test.ts +376 -0
- package/src/__tests__/utils/IPFSClient.test.ts +262 -0
- package/src/__tests__/utils/NonceManager.test.ts +205 -0
- package/src/__tests__/utils/canonicalJson.test.ts +153 -0
- package/src/abi/ACTPKernel.json +1340 -0
- package/src/abi/ERC20.json +40 -0
- package/src/abi/EscrowVault.json +66 -0
- package/src/builders/DeliveryProofBuilder.ts +326 -0
- package/src/builders/QuoteBuilder.ts +483 -0
- package/src/builders/index.ts +17 -0
- package/src/config/networks.ts +165 -0
- package/src/errors/index.ts +130 -0
- package/src/index.ts +108 -0
- package/src/protocol/ACTPKernel.ts +625 -0
- package/src/protocol/EASHelper.ts +197 -0
- package/src/protocol/EscrowVault.ts +237 -0
- package/src/protocol/EventMonitor.ts +161 -0
- package/src/protocol/MessageSigner.ts +336 -0
- package/src/protocol/ProofGenerator.ts +119 -0
- package/src/protocol/QuoteBuilder.ts +15 -0
- package/src/types/eip712.ts +175 -0
- package/src/types/escrow.ts +26 -0
- package/src/types/index.ts +10 -0
- package/src/types/message.ts +145 -0
- package/src/types/state.ts +77 -0
- package/src/types/transaction.ts +54 -0
- package/src/utils/IPFSClient.ts +248 -0
- package/src/utils/NonceManager.ts +293 -0
- package/src/utils/ReceivedNonceTracker.ts +397 -0
- package/src/utils/canonicalJson.ts +38 -0
- package/src/utils/computeTypeHash.ts +50 -0
- package/src/utils/validation.ts +82 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Contract, Signer, AbiCoder, zeroPadValue } from 'ethers';
|
|
2
|
+
import { DeliveryProof } from '../types';
|
|
3
|
+
import { deliveryProofDataFromProof } from '../types/eip712';
|
|
4
|
+
|
|
5
|
+
export interface EASConfig {
|
|
6
|
+
contractAddress: string;
|
|
7
|
+
deliveryProofSchemaId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AttestationResponse {
|
|
11
|
+
uid: string;
|
|
12
|
+
transactionHash: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* EASHelper - utility wrapper for Ethereum Attestation Service interactions
|
|
17
|
+
*/
|
|
18
|
+
const EAS_ABI = [
|
|
19
|
+
'event Attested(address indexed attester, bytes32 indexed uid, bytes32 indexed schema)',
|
|
20
|
+
'function attest(tuple(bytes32 schema, tuple(address recipient, uint64 expirationTime, bool revocable, bytes32 refUID, bytes data) data) request) external returns (bytes32)',
|
|
21
|
+
'function revoke(tuple(bytes32 schema, bytes32 uid) request) external returns (bytes32)',
|
|
22
|
+
'function getAttestation(bytes32 uid) external view returns (tuple(bytes32 uid, bytes32 schema, address recipient, address attester, uint64 time, uint64 expirationTime, bool revocable, bytes32 refUID, bytes data, uint32 bump))'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export class EASHelper {
|
|
26
|
+
private readonly eas: Contract;
|
|
27
|
+
|
|
28
|
+
constructor(signer: Signer, private readonly config: EASConfig) {
|
|
29
|
+
this.eas = new Contract(config.contractAddress, EAS_ABI, signer);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create an attestation for a delivery proof. Returns the attestation UID and transaction hash.
|
|
34
|
+
*/
|
|
35
|
+
async attestDeliveryProof(
|
|
36
|
+
proof: DeliveryProof,
|
|
37
|
+
recipient: string,
|
|
38
|
+
options?: { expirationTime?: number; revocable?: boolean }
|
|
39
|
+
): Promise<AttestationResponse> {
|
|
40
|
+
const { expirationTime = 0, revocable = true } = options || {};
|
|
41
|
+
const proofData = deliveryProofDataFromProof(proof);
|
|
42
|
+
|
|
43
|
+
const abiCoder = AbiCoder.defaultAbiCoder();
|
|
44
|
+
const encodedData = abiCoder.encode(
|
|
45
|
+
['bytes32', 'bytes32', 'uint256', 'string', 'uint256', 'string'],
|
|
46
|
+
[
|
|
47
|
+
proofData.txId,
|
|
48
|
+
proofData.contentHash,
|
|
49
|
+
proofData.timestamp,
|
|
50
|
+
proofData.deliveryUrl || '',
|
|
51
|
+
proofData.size,
|
|
52
|
+
proofData.mimeType
|
|
53
|
+
]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const tx = await this.eas.attest({
|
|
57
|
+
schema: this.config.deliveryProofSchemaId,
|
|
58
|
+
data: {
|
|
59
|
+
recipient,
|
|
60
|
+
expirationTime,
|
|
61
|
+
revocable,
|
|
62
|
+
refUID: proof.txId,
|
|
63
|
+
data: encodedData
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const receipt = await tx.wait();
|
|
68
|
+
// ethers v6: events → logs, and logs are parsed differently
|
|
69
|
+
const attestedLog = receipt?.logs?.find((log: any) => {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = this.eas.interface.parseLog(log);
|
|
72
|
+
return parsed?.name === 'Attested';
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const uid = attestedLog
|
|
79
|
+
? this.eas.interface.parseLog(attestedLog)?.args?.uid
|
|
80
|
+
: zeroPadValue('0x00', 32);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
uid,
|
|
84
|
+
transactionHash: receipt.transactionHash
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Revoke a previously issued attestation by UID.
|
|
90
|
+
*/
|
|
91
|
+
async revokeAttestation(uid: string): Promise<string> {
|
|
92
|
+
const tx = await this.eas.revoke({
|
|
93
|
+
schema: this.config.deliveryProofSchemaId,
|
|
94
|
+
uid
|
|
95
|
+
});
|
|
96
|
+
const receipt = await tx.wait();
|
|
97
|
+
return receipt.transactionHash;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fetch attestation data from the EAS contract.
|
|
102
|
+
*/
|
|
103
|
+
async getAttestation(uid: string) {
|
|
104
|
+
return await this.eas.getAttestation(uid);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Verify that a delivery attestation belongs to the specified transaction.
|
|
109
|
+
*
|
|
110
|
+
* SECURITY: ACTPKernel V1 accepts any bytes32 as attestationUID without validation.
|
|
111
|
+
* This means a malicious provider could submit attestation from Transaction A
|
|
112
|
+
* for Transaction B. This method provides SDK-side protection by verifying:
|
|
113
|
+
*
|
|
114
|
+
* 1. Attestation exists and is not revoked
|
|
115
|
+
* 2. Attestation uses the canonical delivery schema UID
|
|
116
|
+
* 3. Attestation's txId matches the expected transaction ID
|
|
117
|
+
* 4. Attestation has not expired
|
|
118
|
+
*
|
|
119
|
+
* @param txId - Expected transaction ID (bytes32)
|
|
120
|
+
* @param attestationUID - Attestation UID to verify (bytes32)
|
|
121
|
+
* @returns true if attestation is valid for this transaction, false otherwise
|
|
122
|
+
* @throws Error if attestation is revoked, expired, schema mismatch, or txId mismatch
|
|
123
|
+
*/
|
|
124
|
+
async verifyDeliveryAttestation(
|
|
125
|
+
txId: string,
|
|
126
|
+
attestationUID: string
|
|
127
|
+
): Promise<boolean> {
|
|
128
|
+
// 1. Fetch attestation from EAS contract
|
|
129
|
+
const attestation = await this.eas.getAttestation(attestationUID);
|
|
130
|
+
|
|
131
|
+
// 2. Check if attestation exists (uid should match)
|
|
132
|
+
if (attestation.uid !== attestationUID) {
|
|
133
|
+
throw new Error(`Attestation not found: ${attestationUID}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. Check schema UID matches canonical delivery schema (B2 blocker fix)
|
|
137
|
+
// This prevents accepting attestations from unrelated EAS schemas
|
|
138
|
+
if (attestation.schema !== this.config.deliveryProofSchemaId) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Schema UID mismatch: expected canonical delivery schema ${this.config.deliveryProofSchemaId}, ` +
|
|
141
|
+
`got ${attestation.schema}. Attestation may be from a different schema!`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 4. Check revocation - EAS uses revocationTime field (not revoked boolean)
|
|
146
|
+
// revocationTime = 0 means not revoked
|
|
147
|
+
// revocationTime > 0 means revoked at that timestamp
|
|
148
|
+
// NOTE: attestation.revoked field does NOT exist! (see genetic-memory.md)
|
|
149
|
+
const isRevoked = attestation.revocationTime > 0n;
|
|
150
|
+
if (isRevoked) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Attestation has been revoked: ${attestationUID} (revoked at timestamp ${attestation.revocationTime})`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 5. Check expiration
|
|
157
|
+
// expirationTime = 0 means no expiration
|
|
158
|
+
// expirationTime > 0 means expires at that timestamp
|
|
159
|
+
if (attestation.expirationTime > 0n) {
|
|
160
|
+
const now = Math.floor(Date.now() / 1000);
|
|
161
|
+
if (Number(attestation.expirationTime) < now) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Attestation has expired: ${attestationUID} (expired at ${attestation.expirationTime})`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 6. Decode attestation data to extract txId
|
|
169
|
+
// Schema: bytes32 txId, bytes32 contentHash, uint256 timestamp, string deliveryUrl, uint256 size, string mimeType
|
|
170
|
+
let attestedTxId: string;
|
|
171
|
+
try {
|
|
172
|
+
const abiCoder = AbiCoder.defaultAbiCoder();
|
|
173
|
+
const decoded = abiCoder.decode(
|
|
174
|
+
['bytes32', 'bytes32', 'uint256', 'string', 'uint256', 'string'],
|
|
175
|
+
attestation.data
|
|
176
|
+
);
|
|
177
|
+
attestedTxId = decoded[0]; // First field is txId
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Attestation data format mismatch: cannot decode attestation ${attestationUID}. ` +
|
|
181
|
+
`Expected AIP-4 delivery proof schema format. ` +
|
|
182
|
+
`Original error: ${error.message}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 7. Verify attestation txId matches expected transaction ID
|
|
187
|
+
if (attestedTxId !== txId) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Attestation txId mismatch: expected ${txId}, got ${attestedTxId}. ` +
|
|
190
|
+
`Provider may be attempting to use attestation from different transaction!`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// All checks passed
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Contract, Signer } from 'ethers';
|
|
2
|
+
import EscrowVaultABI from '../abi/EscrowVault.json';
|
|
3
|
+
import ERC20ABI from '../abi/ERC20.json';
|
|
4
|
+
import { Escrow } from '../types';
|
|
5
|
+
import { TransactionRevertedError, ValidationError } from '../errors';
|
|
6
|
+
import {
|
|
7
|
+
validateAddress,
|
|
8
|
+
validateAmount,
|
|
9
|
+
validateTxId
|
|
10
|
+
} from '../utils/validation';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gas options for transactions
|
|
14
|
+
*/
|
|
15
|
+
interface GasOptions {
|
|
16
|
+
maxFeePerGas?: bigint;
|
|
17
|
+
maxPriorityFeePerGas?: bigint;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* EscrowVault - Escrow contract wrapper
|
|
22
|
+
*
|
|
23
|
+
* IMPORTANT: Per AIP-3 specification, escrow creation happens atomically
|
|
24
|
+
* inside ACTPKernel.linkEscrow(). This module provides read-only access
|
|
25
|
+
* to escrow state and helper methods for USDC approvals.
|
|
26
|
+
*
|
|
27
|
+
* Workflow (per AIP-3):
|
|
28
|
+
* 1. Consumer approves USDC to EscrowVault address (use approveToken)
|
|
29
|
+
* 2. Consumer calls ACTPKernel.linkEscrow(txId, escrowVault, escrowId)
|
|
30
|
+
* 3. Kernel internally calls EscrowVault.createEscrow() (onlyKernel modifier)
|
|
31
|
+
* 4. Escrow pulls USDC from consumer and auto-transitions to COMMITTED
|
|
32
|
+
*
|
|
33
|
+
* Reference: AIP-3 §3.2 (Escrow Linking Workflow), lines 258-336
|
|
34
|
+
*/
|
|
35
|
+
export class EscrowVault {
|
|
36
|
+
private contract: Contract;
|
|
37
|
+
private readonly gasSettings?: GasOptions;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly address: string,
|
|
41
|
+
private readonly signer: Signer,
|
|
42
|
+
gasSettings?: GasOptions
|
|
43
|
+
) {
|
|
44
|
+
this.contract = new Contract(address, EscrowVaultABI, signer);
|
|
45
|
+
this.gasSettings = gasSettings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get gas buffer multiplier based on operation complexity
|
|
50
|
+
* V6 Security Enhancement: Operation-specific gas buffers
|
|
51
|
+
* Reference: SDK_SECURITY_ANALYSIS-Ultra-Think.md Lines 326-337
|
|
52
|
+
*/
|
|
53
|
+
private getGasBufferMultiplier(operation: string): number {
|
|
54
|
+
const buffers: Record<string, number> = {
|
|
55
|
+
'releaseEscrow': 1.30, // 30% - Multi-recipient disbursement
|
|
56
|
+
'approveToken': 1.20 // 20% - Standard ERC20 approval
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return buffers[operation] || 1.20; // Default 20% for unknown operations
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build transaction options with gas settings and estimated gas
|
|
64
|
+
* V6 Enhancement: Dynamic buffer based on operation type
|
|
65
|
+
*/
|
|
66
|
+
private buildTxOptions(estimatedGas: bigint, operation: string = 'default'): any {
|
|
67
|
+
const bufferMultiplier = this.getGasBufferMultiplier(operation);
|
|
68
|
+
|
|
69
|
+
const options: any = {
|
|
70
|
+
gasLimit: (estimatedGas * BigInt(Math.round(bufferMultiplier * 100))) / 100n
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (this.gasSettings?.maxFeePerGas) {
|
|
74
|
+
options.maxFeePerGas = this.gasSettings.maxFeePerGas;
|
|
75
|
+
}
|
|
76
|
+
if (this.gasSettings?.maxPriorityFeePerGas) {
|
|
77
|
+
options.maxPriorityFeePerGas = this.gasSettings.maxPriorityFeePerGas;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return options;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get escrow vault address
|
|
85
|
+
*/
|
|
86
|
+
getAddress(): string {
|
|
87
|
+
return this.address;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Approve USDC token for escrow creation
|
|
92
|
+
*
|
|
93
|
+
* IMPORTANT: Call this BEFORE ACTPKernel.linkEscrow()
|
|
94
|
+
* The consumer must approve EscrowVault to pull USDC when linkEscrow() is called
|
|
95
|
+
*
|
|
96
|
+
* @param tokenAddress - USDC contract address
|
|
97
|
+
* @param amount - Amount to approve (in USDC wei, 6 decimals)
|
|
98
|
+
* @throws {ValidationError} If inputs are invalid
|
|
99
|
+
* @throws {TransactionRevertedError} If approval fails
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* // Approve 100 USDC for escrow
|
|
104
|
+
* const amount = ethers.parseUnits('100', 6);
|
|
105
|
+
* await client.escrow.approveToken(BASE_SEPOLIA.contracts.usdc, amount);
|
|
106
|
+
*
|
|
107
|
+
* // Now call linkEscrow via Kernel
|
|
108
|
+
* const escrowId = ethers.id(`escrow-${Date.now()}`);
|
|
109
|
+
* await client.kernel.linkEscrow(txId, escrowVault, escrowId);
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
async approveToken(tokenAddress: string, amount: bigint): Promise<void> {
|
|
113
|
+
validateAddress(tokenAddress, 'tokenAddress');
|
|
114
|
+
validateAmount(amount, 'amount');
|
|
115
|
+
|
|
116
|
+
const tokenContract = new Contract(tokenAddress, ERC20ABI, this.signer);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Check current allowance
|
|
120
|
+
const currentAllowance = await tokenContract.allowance(
|
|
121
|
+
await this.signer.getAddress(),
|
|
122
|
+
this.address
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Only approve if needed
|
|
126
|
+
if (currentAllowance < amount) {
|
|
127
|
+
const approveFunc = tokenContract.getFunction('approve');
|
|
128
|
+
|
|
129
|
+
// USDC-compatible approval pattern:
|
|
130
|
+
// If any residual allowance exists, reset to zero first
|
|
131
|
+
if (currentAllowance > 0n) {
|
|
132
|
+
const resetGas = await approveFunc.estimateGas(this.address, 0);
|
|
133
|
+
const resetTx = await approveFunc(this.address, 0, this.buildTxOptions(resetGas, 'approveToken'));
|
|
134
|
+
await resetTx.wait();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Now set the new allowance
|
|
138
|
+
const approveGas = await approveFunc.estimateGas(this.address, amount);
|
|
139
|
+
const approveTx = await approveFunc(this.address, amount, this.buildTxOptions(approveGas, 'approveToken'));
|
|
140
|
+
await approveTx.wait();
|
|
141
|
+
}
|
|
142
|
+
} catch (error: any) {
|
|
143
|
+
throw new TransactionRevertedError(
|
|
144
|
+
error.transactionHash,
|
|
145
|
+
`Token approval failed: ${error.reason || error.message}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get escrow details
|
|
152
|
+
*/
|
|
153
|
+
async getEscrow(escrowId: string): Promise<Escrow> {
|
|
154
|
+
const escrowData = await this.contract.escrows(escrowId);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
escrowId,
|
|
158
|
+
kernel: escrowData.kernel,
|
|
159
|
+
txId: escrowData.txId,
|
|
160
|
+
token: escrowData.token,
|
|
161
|
+
amount: escrowData.amount,
|
|
162
|
+
beneficiary: escrowData.beneficiary,
|
|
163
|
+
createdAt: 0, // Not exposed in minimal ABI
|
|
164
|
+
released: escrowData.released
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get escrow balance
|
|
170
|
+
*/
|
|
171
|
+
async getEscrowBalance(escrowId: string): Promise<bigint> {
|
|
172
|
+
const escrow = await this.getEscrow(escrowId);
|
|
173
|
+
return escrow.amount;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Release escrow to recipients
|
|
178
|
+
* Note: Only callable by authorized kernel
|
|
179
|
+
*/
|
|
180
|
+
async releaseEscrow(
|
|
181
|
+
escrowId: string,
|
|
182
|
+
recipients: string[],
|
|
183
|
+
amounts: bigint[]
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
// Input validation
|
|
186
|
+
validateTxId(escrowId, 'escrowId');
|
|
187
|
+
|
|
188
|
+
if (recipients.length !== amounts.length) {
|
|
189
|
+
throw new ValidationError('recipients/amounts', 'Recipients and amounts length mismatch');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (recipients.length === 0) {
|
|
193
|
+
throw new ValidationError('recipients', 'Must provide at least one recipient');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate each recipient and amount
|
|
197
|
+
recipients.forEach((recipient, i) => {
|
|
198
|
+
validateAddress(recipient, `recipients[${i}]`);
|
|
199
|
+
validateAmount(amounts[i], `amounts[${i}]`);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// ethers v6: use getFunction()
|
|
204
|
+
const disburseFunc = this.contract.getFunction('disburse');
|
|
205
|
+
|
|
206
|
+
// Estimate gas with safety buffer (30% for multi-recipient disbursement)
|
|
207
|
+
const estimatedGas = await disburseFunc.estimateGas(escrowId, recipients, amounts);
|
|
208
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'releaseEscrow');
|
|
209
|
+
|
|
210
|
+
const tx = await disburseFunc(escrowId, recipients, amounts, txOptions);
|
|
211
|
+
|
|
212
|
+
await tx.wait();
|
|
213
|
+
} catch (error: any) {
|
|
214
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check token balance
|
|
220
|
+
*/
|
|
221
|
+
async getTokenBalance(tokenAddress: string, account: string): Promise<bigint> {
|
|
222
|
+
const tokenContract = new Contract(tokenAddress, ERC20ABI, this.signer);
|
|
223
|
+
return await tokenContract.balanceOf(account);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check token allowance
|
|
228
|
+
*/
|
|
229
|
+
async getTokenAllowance(
|
|
230
|
+
tokenAddress: string,
|
|
231
|
+
owner: string,
|
|
232
|
+
spender: string
|
|
233
|
+
): Promise<bigint> {
|
|
234
|
+
const tokenContract = new Contract(tokenAddress, ERC20ABI, this.signer);
|
|
235
|
+
return await tokenContract.allowance(owner, spender);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Contract, EventLog } from 'ethers';
|
|
2
|
+
import { State, Transaction } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EventMonitor - Listen to blockchain events
|
|
6
|
+
*/
|
|
7
|
+
export class EventMonitor {
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly kernelContract: Contract,
|
|
10
|
+
_escrowContract: Contract
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Watch transaction state changes
|
|
15
|
+
* Returns cleanup function to stop watching
|
|
16
|
+
*/
|
|
17
|
+
watchTransaction(txId: string, callback: (state: State) => void): () => void {
|
|
18
|
+
const filter = this.kernelContract.filters.StateTransitioned(txId);
|
|
19
|
+
|
|
20
|
+
const listener = (_eventTxId: string, _from: number, to: number) => {
|
|
21
|
+
callback(to as State);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.kernelContract.on(filter, listener);
|
|
25
|
+
|
|
26
|
+
// Return cleanup function
|
|
27
|
+
return () => {
|
|
28
|
+
this.kernelContract.off(filter, listener);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wait for specific state
|
|
34
|
+
*/
|
|
35
|
+
async waitForState(
|
|
36
|
+
txId: string,
|
|
37
|
+
targetState: State,
|
|
38
|
+
timeoutMs: number = 60000
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
cleanup();
|
|
43
|
+
reject(new Error(`Timeout waiting for state ${State[targetState]}`));
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
|
|
46
|
+
const cleanup = this.watchTransaction(txId, (state) => {
|
|
47
|
+
if (state === targetState) {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
cleanup();
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all transactions for an address
|
|
58
|
+
* Fixed: Correct filter parameters (txId, provider, requester, amount)
|
|
59
|
+
*/
|
|
60
|
+
async getTransactionHistory(
|
|
61
|
+
address: string,
|
|
62
|
+
role: 'requester' | 'provider' = 'requester'
|
|
63
|
+
): Promise<Transaction[]> {
|
|
64
|
+
// TransactionCreated event signature: (bytes32 indexed txId, address indexed provider, address indexed requester, uint256 amount)
|
|
65
|
+
// Filter format: TransactionCreated(txId, provider, requester)
|
|
66
|
+
const filter =
|
|
67
|
+
role === 'requester'
|
|
68
|
+
? this.kernelContract.filters.TransactionCreated(null, null, address) // Match requester
|
|
69
|
+
: this.kernelContract.filters.TransactionCreated(null, address, null); // Match provider
|
|
70
|
+
|
|
71
|
+
const events = await this.kernelContract.queryFilter(filter);
|
|
72
|
+
|
|
73
|
+
return Promise.all(
|
|
74
|
+
events.map(async (event) => {
|
|
75
|
+
// ethers v6: EventLog has args, Log does not
|
|
76
|
+
if (!('args' in event)) {
|
|
77
|
+
throw new Error('Event does not contain args (not an EventLog)');
|
|
78
|
+
}
|
|
79
|
+
const txId = (event as EventLog).args?.transactionId;
|
|
80
|
+
const txData = await this.kernelContract.transactions(txId);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
txId: txData.transactionId,
|
|
84
|
+
requester: txData.requester,
|
|
85
|
+
provider: txData.provider,
|
|
86
|
+
amount: txData.amount,
|
|
87
|
+
state: txData.state as State,
|
|
88
|
+
createdAt: Number(txData.createdAt),
|
|
89
|
+
deadline: Number(txData.deadline),
|
|
90
|
+
disputeWindow: Number(txData.disputeWindow),
|
|
91
|
+
escrowContract: txData.escrowContract,
|
|
92
|
+
escrowId: txData.escrowId,
|
|
93
|
+
metadata: txData.serviceHash
|
|
94
|
+
};
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Subscribe to transaction creation events
|
|
101
|
+
* Fixed: Correct event parameter order (txId, provider, requester, amount)
|
|
102
|
+
*/
|
|
103
|
+
onTransactionCreated(
|
|
104
|
+
callback: (tx: { txId: string; provider: string; requester: string; amount: bigint }) => void
|
|
105
|
+
): () => void {
|
|
106
|
+
const filter = this.kernelContract.filters.TransactionCreated();
|
|
107
|
+
|
|
108
|
+
// Event signature: TransactionCreated(bytes32 indexed txId, address indexed provider, address indexed requester, uint256 amount)
|
|
109
|
+
const listener = async (
|
|
110
|
+
txId: string,
|
|
111
|
+
provider: string,
|
|
112
|
+
requester: string,
|
|
113
|
+
amount: bigint
|
|
114
|
+
) => {
|
|
115
|
+
callback({ txId, provider, requester, amount });
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.kernelContract.on(filter, listener);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
this.kernelContract.off(filter, listener);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Subscribe to state change events
|
|
127
|
+
*/
|
|
128
|
+
onStateChanged(
|
|
129
|
+
callback: (txId: string, from: State, to: State) => void
|
|
130
|
+
): () => void {
|
|
131
|
+
const filter = this.kernelContract.filters.StateTransitioned();
|
|
132
|
+
|
|
133
|
+
const listener = (txId: string, from: number, to: number) => {
|
|
134
|
+
callback(txId, from as State, to as State);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this.kernelContract.on(filter, listener);
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
this.kernelContract.off(filter, listener);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Subscribe to escrow release events
|
|
146
|
+
*/
|
|
147
|
+
onEscrowReleased(callback: (txId: string, amount: bigint) => void): () => void {
|
|
148
|
+
const filter = this.kernelContract.filters.EscrowReleased();
|
|
149
|
+
|
|
150
|
+
const listener = (txId: string, amount: bigint) => {
|
|
151
|
+
callback(txId, amount);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
this.kernelContract.on(filter, listener);
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
this.kernelContract.off(filter, listener);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|