@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,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACTP Message Types
|
|
3
|
+
* Reference: Yellow Paper §4-10 (AIPs)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base ACTP message structure
|
|
8
|
+
*/
|
|
9
|
+
export interface ACTPMessage {
|
|
10
|
+
type: string;
|
|
11
|
+
version: string;
|
|
12
|
+
from: string; // DID
|
|
13
|
+
to: string; // DID
|
|
14
|
+
timestamp: number;
|
|
15
|
+
nonce: string;
|
|
16
|
+
signature?: string;
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Quote Request (AIP-2)
|
|
22
|
+
* Reference: Yellow Paper §6.2.1
|
|
23
|
+
*/
|
|
24
|
+
export interface QuoteRequest extends ACTPMessage {
|
|
25
|
+
type: 'quote.request';
|
|
26
|
+
serviceRequest: {
|
|
27
|
+
capabilityType: string;
|
|
28
|
+
parameters: Record<string, any>;
|
|
29
|
+
deliveryRequirements: {
|
|
30
|
+
deadline: string;
|
|
31
|
+
maxDeliveryTime: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
budgetConstraints: {
|
|
35
|
+
maxPrice: string;
|
|
36
|
+
currency: 'USDC';
|
|
37
|
+
};
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Quote Response (AIP-2)
|
|
43
|
+
* Reference: Yellow Paper §6.2.2
|
|
44
|
+
*/
|
|
45
|
+
export interface QuoteResponse extends ACTPMessage {
|
|
46
|
+
type: 'quote.response';
|
|
47
|
+
inResponseTo: string;
|
|
48
|
+
quoteId: string;
|
|
49
|
+
pricing: {
|
|
50
|
+
totalPrice: string;
|
|
51
|
+
currency: 'USDC';
|
|
52
|
+
breakdown: Array<{ item: string; amount: string }>;
|
|
53
|
+
platformFee: string;
|
|
54
|
+
};
|
|
55
|
+
sla: {
|
|
56
|
+
successRateGuarantee: number;
|
|
57
|
+
refundPolicy: string;
|
|
58
|
+
};
|
|
59
|
+
expiresAt: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Delivery Proof (AIP-4) - DEPRECATED
|
|
64
|
+
* @deprecated Use DeliveryProofMessage instead (AIP-4 v1.1)
|
|
65
|
+
*/
|
|
66
|
+
export interface DeliveryProof {
|
|
67
|
+
type: 'delivery.proof';
|
|
68
|
+
txId: string;
|
|
69
|
+
contentHash: string;
|
|
70
|
+
timestamp: number;
|
|
71
|
+
deliveryUrl?: string;
|
|
72
|
+
metadata: {
|
|
73
|
+
size: number;
|
|
74
|
+
mimeType: string;
|
|
75
|
+
[key: string]: any;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Delivery Proof Message (AIP-4 v1.1)
|
|
81
|
+
* Reference: AIP-4 §3.2, §8.1
|
|
82
|
+
* Complete schema for delivery proofs with EAS attestations
|
|
83
|
+
*/
|
|
84
|
+
export interface DeliveryProofMessage {
|
|
85
|
+
type: 'agirails.delivery.v1';
|
|
86
|
+
version: string; // Semantic version (e.g., "1.0.0")
|
|
87
|
+
txId: string; // bytes32 (0x-prefixed)
|
|
88
|
+
provider: string; // DID (e.g., "did:ethr:84532:0x...")
|
|
89
|
+
consumer: string; // DID
|
|
90
|
+
resultCID: string; // IPFS CID (CIDv1, base32, e.g., "bafybeig...")
|
|
91
|
+
resultHash: string; // Keccak256 hash of canonical result JSON
|
|
92
|
+
metadata?: {
|
|
93
|
+
executionTime?: number; // Seconds
|
|
94
|
+
outputFormat?: string; // MIME type
|
|
95
|
+
outputSize?: number; // Bytes
|
|
96
|
+
notes?: string; // Max 500 chars
|
|
97
|
+
};
|
|
98
|
+
easAttestationUID: string; // bytes32 (0x-prefixed)
|
|
99
|
+
deliveredAt: number; // Unix timestamp (seconds)
|
|
100
|
+
chainId: number; // 84532 (Base Sepolia) or 8453 (Base Mainnet)
|
|
101
|
+
nonce: number; // Monotonically increasing
|
|
102
|
+
signature: string; // EIP-712 signature (0x-prefixed, 130 chars)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* EAS Attestation Data for delivery proofs
|
|
107
|
+
*/
|
|
108
|
+
export interface EASAttestationData {
|
|
109
|
+
schema: string; // EAS schema UID
|
|
110
|
+
recipient: string; // Provider address
|
|
111
|
+
expirationTime: number; // Unix timestamp (0 for no expiration)
|
|
112
|
+
revocable: boolean;
|
|
113
|
+
refUID: string; // Reference to transaction
|
|
114
|
+
data: string; // ABI-encoded attestation data
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Quote Message (AIP-2 v1.0)
|
|
119
|
+
* Reference: AIP-2 §2.1, §2.2
|
|
120
|
+
* Price quote submitted by provider for negotiated transactions
|
|
121
|
+
*/
|
|
122
|
+
export interface QuoteMessageV2 {
|
|
123
|
+
type: 'agirails.quote.v1';
|
|
124
|
+
version: '1.0.0';
|
|
125
|
+
txId: string; // bytes32 (0x-prefixed)
|
|
126
|
+
provider: string; // DID (e.g., "did:ethr:84532:0x...")
|
|
127
|
+
consumer: string; // DID
|
|
128
|
+
quotedAmount: string; // Provider's quoted price (base units, string to avoid JS overflow)
|
|
129
|
+
originalAmount: string; // Consumer's original offer from AIP-1
|
|
130
|
+
maxPrice: string; // Consumer's maximum acceptable price from AIP-1
|
|
131
|
+
currency: string; // Currently "USDC" only
|
|
132
|
+
decimals: number; // Token decimals (6 for USDC)
|
|
133
|
+
quotedAt: number; // Unix timestamp (seconds)
|
|
134
|
+
expiresAt: number; // Unix timestamp (seconds)
|
|
135
|
+
justification?: {
|
|
136
|
+
reason?: string;
|
|
137
|
+
estimatedTime?: number;
|
|
138
|
+
computeCost?: number;
|
|
139
|
+
breakdown?: Record<string, any>;
|
|
140
|
+
};
|
|
141
|
+
chainId: number; // 84532 (Base Sepolia) or 8453 (Base Mainnet)
|
|
142
|
+
nonce: number; // Monotonically increasing per provider + message type
|
|
143
|
+
signature: string; // EIP-712 signature (0x-prefixed, 130 chars)
|
|
144
|
+
}
|
|
145
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACTP State Machine
|
|
3
|
+
* Reference: Yellow Paper §3.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum State {
|
|
7
|
+
INITIATED = 0,
|
|
8
|
+
QUOTED = 1,
|
|
9
|
+
COMMITTED = 2,
|
|
10
|
+
IN_PROGRESS = 3,
|
|
11
|
+
DELIVERED = 4,
|
|
12
|
+
SETTLED = 5,
|
|
13
|
+
DISPUTED = 6,
|
|
14
|
+
CANCELLED = 7
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class StateMachine {
|
|
18
|
+
/**
|
|
19
|
+
* Valid state transitions per Yellow Paper §3.2.2
|
|
20
|
+
*/
|
|
21
|
+
private static readonly TRANSITIONS: Record<State, State[]> = {
|
|
22
|
+
[State.INITIATED]: [State.QUOTED, State.COMMITTED, State.CANCELLED], // Allow direct INITIATED → COMMITTED (AIP-3)
|
|
23
|
+
[State.QUOTED]: [State.COMMITTED, State.CANCELLED],
|
|
24
|
+
[State.COMMITTED]: [State.IN_PROGRESS, State.CANCELLED],
|
|
25
|
+
[State.IN_PROGRESS]: [State.DELIVERED, State.DISPUTED],
|
|
26
|
+
[State.DELIVERED]: [State.SETTLED, State.DISPUTED],
|
|
27
|
+
[State.DISPUTED]: [State.SETTLED, State.CANCELLED],
|
|
28
|
+
[State.SETTLED]: [], // Terminal state
|
|
29
|
+
[State.CANCELLED]: [] // Terminal state
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if state transition is valid
|
|
34
|
+
*/
|
|
35
|
+
static isValidTransition(from: State, to: State): boolean {
|
|
36
|
+
return this.TRANSITIONS[from]?.includes(to) ?? false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if state is terminal (no further transitions)
|
|
41
|
+
*/
|
|
42
|
+
static isTerminalState(state: State): boolean {
|
|
43
|
+
return state === State.SETTLED || state === State.CANCELLED;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get human-readable state name
|
|
48
|
+
*/
|
|
49
|
+
static getStateName(state: State): string {
|
|
50
|
+
return State[state];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all valid next states from current state
|
|
55
|
+
*/
|
|
56
|
+
static getNextValidStates(currentState: State): State[] {
|
|
57
|
+
return this.TRANSITIONS[currentState] || [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate state transition or throw error
|
|
62
|
+
*/
|
|
63
|
+
static validateTransition(from: State, to: State): void {
|
|
64
|
+
if (!this.isValidTransition(from, to)) {
|
|
65
|
+
const validStates = this.getNextValidStates(from)
|
|
66
|
+
.map(s => State[s])
|
|
67
|
+
.join(', ');
|
|
68
|
+
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid state transition: ${State[from]} → ${State[to]}. ` +
|
|
71
|
+
`Valid transitions from ${State[from]}: ${validStates || 'none (terminal state)'}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { State } from './state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ACTP Transaction
|
|
5
|
+
* Reference: Yellow Paper §3.1
|
|
6
|
+
*/
|
|
7
|
+
export interface Transaction {
|
|
8
|
+
txId: string;
|
|
9
|
+
requester: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
amount: bigint;
|
|
12
|
+
state: State;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
deadline: number;
|
|
15
|
+
disputeWindow: number;
|
|
16
|
+
escrowContract: string;
|
|
17
|
+
escrowId: string;
|
|
18
|
+
metadata: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parameters for creating a new transaction
|
|
23
|
+
*/
|
|
24
|
+
export interface CreateTransactionParams {
|
|
25
|
+
provider: string;
|
|
26
|
+
requester: string;
|
|
27
|
+
amount: bigint;
|
|
28
|
+
deadline: number;
|
|
29
|
+
disputeWindow: number;
|
|
30
|
+
metadata?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Dispute resolution split
|
|
35
|
+
*/
|
|
36
|
+
export interface DisputeResolution {
|
|
37
|
+
requesterAmount: bigint;
|
|
38
|
+
providerAmount: bigint;
|
|
39
|
+
mediatorAmount: bigint;
|
|
40
|
+
mediator?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Economic parameters (fee structure)
|
|
45
|
+
*/
|
|
46
|
+
export interface EconomicParams {
|
|
47
|
+
baseFeeNumerator: number;
|
|
48
|
+
baseFeeDenominator: number;
|
|
49
|
+
feeRecipient: string;
|
|
50
|
+
requesterPenaltyBps: number;
|
|
51
|
+
providerPenaltyBps: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPFS Client Implementation
|
|
3
|
+
* Wrapper around kubo-rpc-client (formerly ipfs-http-client) for AIP-4 delivery proof uploads
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { create, IPFSHTTPClient, Options } from 'kubo-rpc-client';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* IPFS Client Interface (from DeliveryProofBuilder)
|
|
10
|
+
*/
|
|
11
|
+
export interface IPFSClient {
|
|
12
|
+
/**
|
|
13
|
+
* Upload data to IPFS
|
|
14
|
+
* @param data - JSON string or buffer
|
|
15
|
+
* @returns CIDv1 string (base32, e.g., "bafybeig...")
|
|
16
|
+
*/
|
|
17
|
+
add(data: string | Buffer): Promise<string>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pin content to prevent garbage collection
|
|
21
|
+
* @param cid - IPFS CID
|
|
22
|
+
*/
|
|
23
|
+
pin(cid: string): Promise<void>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Retrieve content from IPFS
|
|
27
|
+
* @param cid - IPFS CID
|
|
28
|
+
* @returns Content as string
|
|
29
|
+
*/
|
|
30
|
+
get(cid: string): Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* IPFS Client Configuration
|
|
35
|
+
*/
|
|
36
|
+
export interface IPFSClientConfig {
|
|
37
|
+
/**
|
|
38
|
+
* IPFS HTTP API endpoint
|
|
39
|
+
* Default: http://localhost:5001 (local IPFS daemon)
|
|
40
|
+
* Production: https://ipfs.infura.io:5001/api/v0 (Infura)
|
|
41
|
+
*/
|
|
42
|
+
url?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* API authentication (for Infura, Pinata, etc.)
|
|
46
|
+
*/
|
|
47
|
+
auth?: {
|
|
48
|
+
username: string;
|
|
49
|
+
password: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* HTTP headers (for API keys)
|
|
54
|
+
*/
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Request timeout (ms)
|
|
59
|
+
* Default: 60000 (60 seconds)
|
|
60
|
+
*/
|
|
61
|
+
timeout?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Default IPFS configurations
|
|
66
|
+
*/
|
|
67
|
+
export const IPFS_CONFIGS = {
|
|
68
|
+
local: {
|
|
69
|
+
url: 'http://localhost:5001'
|
|
70
|
+
},
|
|
71
|
+
infura: {
|
|
72
|
+
url: 'https://ipfs.infura.io:5001/api/v0'
|
|
73
|
+
// Auth required: Set INFURA_PROJECT_ID and INFURA_PROJECT_SECRET
|
|
74
|
+
},
|
|
75
|
+
pinata: {
|
|
76
|
+
url: 'https://api.pinata.cloud'
|
|
77
|
+
// Headers required: Set pinata_api_key and pinata_secret_api_key
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* IPFS HTTP Client Implementation
|
|
83
|
+
* Uses ipfs-http-client library
|
|
84
|
+
*/
|
|
85
|
+
export class IPFSHTTPClientImpl implements IPFSClient {
|
|
86
|
+
private client: IPFSHTTPClient;
|
|
87
|
+
private config: IPFSClientConfig;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create IPFS client
|
|
91
|
+
* @param config - IPFS client configuration
|
|
92
|
+
*/
|
|
93
|
+
constructor(config: IPFSClientConfig = {}) {
|
|
94
|
+
this.config = {
|
|
95
|
+
url: config.url || 'http://localhost:5001',
|
|
96
|
+
timeout: config.timeout || 60000,
|
|
97
|
+
...config
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const options: Options = {
|
|
101
|
+
url: this.config.url,
|
|
102
|
+
timeout: this.config.timeout
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Add authentication if provided
|
|
106
|
+
if (this.config.auth) {
|
|
107
|
+
options.headers = {
|
|
108
|
+
...this.config.headers,
|
|
109
|
+
authorization: 'Basic ' + Buffer.from(
|
|
110
|
+
`${this.config.auth.username}:${this.config.auth.password}`
|
|
111
|
+
).toString('base64')
|
|
112
|
+
};
|
|
113
|
+
} else if (this.config.headers) {
|
|
114
|
+
options.headers = this.config.headers;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.client = create(options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Upload data to IPFS
|
|
122
|
+
* @param data - JSON string or buffer
|
|
123
|
+
* @returns CIDv1 string (base32)
|
|
124
|
+
*/
|
|
125
|
+
async add(data: string | Buffer): Promise<string> {
|
|
126
|
+
try {
|
|
127
|
+
const content = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
|
|
128
|
+
|
|
129
|
+
const result = await this.client.add(content, {
|
|
130
|
+
cidVersion: 1, // Use CIDv1 (base32)
|
|
131
|
+
hashAlg: 'sha2-256',
|
|
132
|
+
pin: true // Auto-pin on upload
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Convert CID to base32 string (e.g., "bafybeig...")
|
|
136
|
+
return result.cid.toString();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`IPFS upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Pin content to prevent garbage collection
|
|
144
|
+
* @param cid - IPFS CID
|
|
145
|
+
*/
|
|
146
|
+
async pin(cid: string): Promise<void> {
|
|
147
|
+
try {
|
|
148
|
+
await this.client.pin.add(cid);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(`IPFS pin failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Retrieve content from IPFS
|
|
156
|
+
* @param cid - IPFS CID
|
|
157
|
+
* @returns Content as string
|
|
158
|
+
*/
|
|
159
|
+
async get(cid: string): Promise<string> {
|
|
160
|
+
try {
|
|
161
|
+
const chunks: Uint8Array[] = [];
|
|
162
|
+
|
|
163
|
+
for await (const chunk of this.client.cat(cid)) {
|
|
164
|
+
chunks.push(chunk);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Concatenate all chunks
|
|
168
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
169
|
+
const result = new Uint8Array(totalLength);
|
|
170
|
+
let offset = 0;
|
|
171
|
+
|
|
172
|
+
for (const chunk of chunks) {
|
|
173
|
+
result.set(chunk, offset);
|
|
174
|
+
offset += chunk.length;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Buffer.from(result).toString('utf-8');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new Error(`IPFS retrieval failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if IPFS daemon is reachable
|
|
185
|
+
* @returns true if connected, false otherwise
|
|
186
|
+
*/
|
|
187
|
+
async isOnline(): Promise<boolean> {
|
|
188
|
+
try {
|
|
189
|
+
await this.client.id();
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get IPFS node ID
|
|
198
|
+
* @returns IPFS node ID
|
|
199
|
+
*/
|
|
200
|
+
async getNodeId(): Promise<string> {
|
|
201
|
+
try {
|
|
202
|
+
const id = await this.client.id();
|
|
203
|
+
return id.id.toString();
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new Error(`Failed to get node ID: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create IPFS client with environment-based configuration
|
|
212
|
+
* Checks for IPFS_URL, INFURA_PROJECT_ID, PINATA_API_KEY env vars
|
|
213
|
+
*/
|
|
214
|
+
export function createIPFSClient(): IPFSClient {
|
|
215
|
+
// Check for Infura credentials
|
|
216
|
+
if (process.env.INFURA_PROJECT_ID && process.env.INFURA_PROJECT_SECRET) {
|
|
217
|
+
return new IPFSHTTPClientImpl({
|
|
218
|
+
url: 'https://ipfs.infura.io:5001/api/v0',
|
|
219
|
+
auth: {
|
|
220
|
+
username: process.env.INFURA_PROJECT_ID,
|
|
221
|
+
password: process.env.INFURA_PROJECT_SECRET
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check for Pinata credentials
|
|
227
|
+
if (process.env.PINATA_API_KEY && process.env.PINATA_SECRET_API_KEY) {
|
|
228
|
+
return new IPFSHTTPClientImpl({
|
|
229
|
+
url: 'https://api.pinata.cloud',
|
|
230
|
+
headers: {
|
|
231
|
+
pinata_api_key: process.env.PINATA_API_KEY,
|
|
232
|
+
pinata_secret_api_key: process.env.PINATA_SECRET_API_KEY
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check for custom IPFS URL
|
|
238
|
+
if (process.env.IPFS_URL) {
|
|
239
|
+
return new IPFSHTTPClientImpl({
|
|
240
|
+
url: process.env.IPFS_URL
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Default to local IPFS daemon
|
|
245
|
+
return new IPFSHTTPClientImpl({
|
|
246
|
+
url: 'http://localhost:5001'
|
|
247
|
+
});
|
|
248
|
+
}
|