@arc402/sdk 0.2.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/README.md +184 -0
- package/dist/agent.d.ts +29 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +95 -0
- package/dist/agreement.d.ts +57 -0
- package/dist/agreement.d.ts.map +1 -0
- package/dist/agreement.js +156 -0
- package/dist/capability.d.ts +17 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +19 -0
- package/dist/channel.d.ts +39 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +160 -0
- package/dist/coldstart.d.ts +15 -0
- package/dist/coldstart.d.ts.map +1 -0
- package/dist/coldstart.js +44 -0
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/contractinteraction.d.ts +47 -0
- package/dist/contractinteraction.d.ts.map +1 -0
- package/dist/contractinteraction.js +80 -0
- package/dist/contracts.d.ts +27 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +187 -0
- package/dist/deliverable.d.ts +118 -0
- package/dist/deliverable.d.ts.map +1 -0
- package/dist/deliverable.js +156 -0
- package/dist/dispute-arbitration.d.ts +42 -0
- package/dist/dispute-arbitration.d.ts.map +1 -0
- package/dist/dispute-arbitration.js +160 -0
- package/dist/governance.d.ts +13 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +15 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/intent.d.ts +13 -0
- package/dist/intent.d.ts.map +1 -0
- package/dist/intent.js +26 -0
- package/dist/metadata.d.ts +55 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +106 -0
- package/dist/migration.d.ts +11 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +38 -0
- package/dist/multiparty.d.ts +56 -0
- package/dist/multiparty.d.ts.map +1 -0
- package/dist/multiparty.js +86 -0
- package/dist/negotiation-guard.d.ts +20 -0
- package/dist/negotiation-guard.d.ts.map +1 -0
- package/dist/negotiation-guard.js +96 -0
- package/dist/negotiation.d.ts +25 -0
- package/dist/negotiation.d.ts.map +1 -0
- package/dist/negotiation.js +102 -0
- package/dist/policy.d.ts +33 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +72 -0
- package/dist/reputation.d.ts +13 -0
- package/dist/reputation.d.ts.map +1 -0
- package/dist/reputation.js +21 -0
- package/dist/session-manager.d.ts +13 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +102 -0
- package/dist/settlement.d.ts +14 -0
- package/dist/settlement.d.ts.map +1 -0
- package/dist/settlement.js +35 -0
- package/dist/sponsorship.d.ts +17 -0
- package/dist/sponsorship.d.ts.map +1 -0
- package/dist/sponsorship.js +19 -0
- package/dist/trust.d.ts +22 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +52 -0
- package/dist/types.d.ts +391 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +119 -0
- package/dist/wallet.d.ts +31 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +83 -0
- package/dist/watchtower.d.ts +60 -0
- package/dist/watchtower.d.ts.map +1 -0
- package/dist/watchtower.js +93 -0
- package/package.json +30 -0
- package/src/agent.ts +122 -0
- package/src/agreement.ts +236 -0
- package/src/capability.ts +18 -0
- package/src/channel.ts +203 -0
- package/src/coldstart.ts +52 -0
- package/src/context.ts +2 -0
- package/src/contractinteraction.d.ts +47 -0
- package/src/contractinteraction.d.ts.map +1 -0
- package/src/contractinteraction.js +81 -0
- package/src/contractinteraction.js.map +1 -0
- package/src/contractinteraction.ts +157 -0
- package/src/contracts.d.ts +27 -0
- package/src/contracts.d.ts.map +1 -0
- package/src/contracts.js +188 -0
- package/src/contracts.js.map +1 -0
- package/src/contracts.ts +186 -0
- package/src/deliverable.ts +231 -0
- package/src/demos/demo-insurance.ts +148 -0
- package/src/demos/demo-multiagent.ts +197 -0
- package/src/demos/demo-research.ts +124 -0
- package/src/dispute-arbitration.ts +196 -0
- package/src/governance.ts +14 -0
- package/src/index.ts +31 -0
- package/src/intent.ts +22 -0
- package/src/metadata.ts +158 -0
- package/src/migration.ts +43 -0
- package/src/multiparty.ts +132 -0
- package/src/negotiation-guard.ts +125 -0
- package/src/negotiation.ts +135 -0
- package/src/policy.ts +71 -0
- package/src/reputation.ts +20 -0
- package/src/session-manager.ts +80 -0
- package/src/settlement.ts +31 -0
- package/src/sponsorship.ts +18 -0
- package/src/trust.ts +43 -0
- package/src/types.d.ts +391 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +113 -0
- package/src/types.js.map +1 -0
- package/src/types.ts +484 -0
- package/src/wallet.ts +86 -0
- package/src/watchtower.ts +124 -0
- package/test/negotiation-signing.test.js +157 -0
- package/test/sdk.test.js +19 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ContractRunner, ethers } from "ethers";
|
|
2
|
+
import { GOVERNANCE_ABI } from "./contracts";
|
|
3
|
+
import { GovernanceTransaction } from "./types";
|
|
4
|
+
|
|
5
|
+
export class GovernanceClient {
|
|
6
|
+
private contract: ethers.Contract;
|
|
7
|
+
constructor(address: string, runner: ContractRunner) { this.contract = new ethers.Contract(address, GOVERNANCE_ABI, runner); }
|
|
8
|
+
async submitTransaction(target: string, value: bigint, data: string) { const tx = await this.contract.submitTransaction(target, value, data); return tx.wait(); }
|
|
9
|
+
async confirmTransaction(txId: bigint) { const tx = await this.contract.confirmTransaction(txId); return tx.wait(); }
|
|
10
|
+
async revokeConfirmation(txId: bigint) { const tx = await this.contract.revokeConfirmation(txId); return tx.wait(); }
|
|
11
|
+
async executeTransaction(txId: bigint) { const tx = await this.contract.executeTransaction(txId); return tx.wait(); }
|
|
12
|
+
async getTransaction(txId: bigint): Promise<GovernanceTransaction> { const raw = await this.contract.getTransaction(txId); return { target: raw.target, value: BigInt(raw.value), data: raw.data, executed: raw.executed, confirmationCount: BigInt(raw.confirmationCount) }; }
|
|
13
|
+
transactionCount() { return this.contract.transactionCount().then(BigInt); }
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { ARC402WalletClient, ARC402Wallet, type ContextBinding } from "./wallet";
|
|
2
|
+
export { ARC402WalletClient as ARC402OperatorClient, ARC402Wallet as ARC402Operator } from "./wallet";
|
|
3
|
+
export { ContractInteractionClient, type IntentPayload, type ContractCallAttestation, type ContractCallResult } from "./contractinteraction";
|
|
4
|
+
export { PolicyClient, PolicyObject, PolicyValidator } from "./policy";
|
|
5
|
+
export { TrustClient, TrustPrimitive } from "./trust";
|
|
6
|
+
export { IntentAttestationClient, IntentAttestation } from "./intent";
|
|
7
|
+
export { SettlementClient, MultiAgentSettlement } from "./settlement";
|
|
8
|
+
export { AgentRegistryClient, type AgentRegistrationInput } from "./agent";
|
|
9
|
+
export { ServiceAgreementClient } from "./agreement";
|
|
10
|
+
export { DisputeArbitrationClient } from "./dispute-arbitration";
|
|
11
|
+
export { ReputationOracleClient } from "./reputation";
|
|
12
|
+
export { SponsorshipAttestationClient } from "./sponsorship";
|
|
13
|
+
export { CapabilityRegistryClient } from "./capability";
|
|
14
|
+
export { GovernanceClient } from "./governance";
|
|
15
|
+
export { signNegotiationMessage, createSignedProposal, createSignedCounter, createSignedAccept, createSignedReject, createNegotiationProposal, createNegotiationCounter, createNegotiationAccept, createNegotiationReject, parseNegotiationMessage } from "./negotiation";
|
|
16
|
+
export { NegotiationGuard } from "./negotiation-guard";
|
|
17
|
+
export { SessionManager } from "./session-manager";
|
|
18
|
+
export { ChannelClient } from "./channel";
|
|
19
|
+
export { AgreementTreeClient, type AgreementTree, type PaymentSplit, type PaymentDistribution, type CreateAgreementResult } from "./multiparty";
|
|
20
|
+
export { WatchtowerClient } from "./watchtower";
|
|
21
|
+
export type { WatchtowerMetadata, WatchtowerStatus } from "./watchtower";
|
|
22
|
+
export { DeliverableClient, DeliverableType, hashDeliverable, hashDeliverableFile, uploadToIPFS, encryptDeliverable, decryptDeliverable, uploadEncryptedIPFS } from "./deliverable";
|
|
23
|
+
export type { DeliverableManifest, DeliverableManifestEntry, IPFSUploadOptions, IPFSUploadResult, EncryptedDeliverable } from "./deliverable";
|
|
24
|
+
export type { NegotiationVerificationResult, NegotiationVerificationError, SignedNegotiationMessage } from "./types";
|
|
25
|
+
export type { ChannelState, Channel, ChannelStatus, OpenChannelParams } from "./types";
|
|
26
|
+
export * from "./contracts";
|
|
27
|
+
export * from "./types";
|
|
28
|
+
export { buildMetadata, validateMetadata, encodeMetadata, decodeMetadata, uploadMetadata, AGENT_METADATA_SCHEMA } from "./metadata";
|
|
29
|
+
export type { AgentMetadata, AgentMetadataModel, AgentMetadataTraining, AgentMetadataPricing, AgentMetadataSla, AgentMetadataContact, AgentMetadataSecurity } from "./metadata";
|
|
30
|
+
export { ColdStartClient } from "./coldstart";
|
|
31
|
+
export { MigrationClient } from "./migration";
|
package/src/intent.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ContractRunner, ethers } from "ethers";
|
|
2
|
+
import { INTENT_ATTESTATION_ABI } from "./contracts";
|
|
3
|
+
import { Intent } from "./types";
|
|
4
|
+
|
|
5
|
+
export class IntentAttestationClient {
|
|
6
|
+
private contract: ethers.Contract;
|
|
7
|
+
constructor(address: string, runner: ContractRunner, private walletAddress: string) {
|
|
8
|
+
this.contract = new ethers.Contract(address, INTENT_ATTESTATION_ABI, runner);
|
|
9
|
+
}
|
|
10
|
+
async create(action: string, reason: string, recipient: string, amount: bigint): Promise<string> {
|
|
11
|
+
const attestationId = ethers.keccak256(ethers.toUtf8Bytes(`${this.walletAddress}:${action}:${reason}:${recipient}:${amount}:${Date.now()}`));
|
|
12
|
+
const tx = await this.contract.attest(attestationId, action, reason, recipient, amount);
|
|
13
|
+
await tx.wait();
|
|
14
|
+
return attestationId;
|
|
15
|
+
}
|
|
16
|
+
verify(attestationId: string, walletAddress: string) { return this.contract.verify(attestationId, walletAddress); }
|
|
17
|
+
async get(attestationId: string): Promise<Intent> {
|
|
18
|
+
const [id, wallet, action, reason, recipient, amount, timestamp] = await this.contract.getAttestation(attestationId);
|
|
19
|
+
return { attestationId: id, wallet, action, reason, recipient, amount: BigInt(amount), timestamp: Number(timestamp) };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class IntentAttestation extends IntentAttestationClient {}
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// ARC-402 Spec 23: Agent Metadata Standard
|
|
2
|
+
|
|
3
|
+
export const AGENT_METADATA_SCHEMA = "arc402.agent-metadata.v1";
|
|
4
|
+
|
|
5
|
+
export interface AgentMetadataModel {
|
|
6
|
+
family?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
provider?: string;
|
|
9
|
+
contextWindow?: number;
|
|
10
|
+
multimodal?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentMetadataTraining {
|
|
14
|
+
disclosure?: string;
|
|
15
|
+
dataCutoff?: string;
|
|
16
|
+
specialisations?: string[];
|
|
17
|
+
synopsis?: string;
|
|
18
|
+
verified?: boolean;
|
|
19
|
+
attestations?: unknown[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AgentMetadataPricing {
|
|
23
|
+
base?: string;
|
|
24
|
+
token?: string;
|
|
25
|
+
currency?: string;
|
|
26
|
+
per?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AgentMetadataSla {
|
|
30
|
+
turnaroundHours?: number;
|
|
31
|
+
availability?: string;
|
|
32
|
+
maxConcurrentJobs?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AgentMetadataContact {
|
|
36
|
+
endpoint?: string;
|
|
37
|
+
relay?: string;
|
|
38
|
+
relayFallbacks?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AgentMetadataSecurity {
|
|
42
|
+
injectionProtection?: boolean;
|
|
43
|
+
envLeakProtection?: boolean;
|
|
44
|
+
attestedSecurityPolicy?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AgentMetadata {
|
|
48
|
+
schema: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
capabilities?: string[];
|
|
52
|
+
model?: AgentMetadataModel;
|
|
53
|
+
training?: AgentMetadataTraining;
|
|
54
|
+
pricing?: AgentMetadataPricing;
|
|
55
|
+
sla?: AgentMetadataSla;
|
|
56
|
+
contact?: AgentMetadataContact;
|
|
57
|
+
security?: AgentMetadataSecurity;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateMetadata(obj: unknown): AgentMetadata {
|
|
61
|
+
if (typeof obj !== "object" || obj === null) {
|
|
62
|
+
throw new Error("metadata must be an object");
|
|
63
|
+
}
|
|
64
|
+
const m = obj as Record<string, unknown>;
|
|
65
|
+
if (typeof m["schema"] !== "string") {
|
|
66
|
+
throw new Error("metadata.schema must be a string");
|
|
67
|
+
}
|
|
68
|
+
if (m["schema"] !== AGENT_METADATA_SCHEMA) {
|
|
69
|
+
throw new Error(`metadata.schema must be "${AGENT_METADATA_SCHEMA}", got "${m["schema"]}"`);
|
|
70
|
+
}
|
|
71
|
+
if (m["name"] !== undefined && typeof m["name"] !== "string") {
|
|
72
|
+
throw new Error("metadata.name must be a string");
|
|
73
|
+
}
|
|
74
|
+
if (m["description"] !== undefined && typeof m["description"] !== "string") {
|
|
75
|
+
throw new Error("metadata.description must be a string");
|
|
76
|
+
}
|
|
77
|
+
if (m["capabilities"] !== undefined) {
|
|
78
|
+
if (!Array.isArray(m["capabilities"]) || !m["capabilities"].every((c: unknown) => typeof c === "string")) {
|
|
79
|
+
throw new Error("metadata.capabilities must be a string array");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return m as unknown as AgentMetadata;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildMetadata(input: Partial<AgentMetadata>): AgentMetadata {
|
|
86
|
+
const meta: AgentMetadata = {
|
|
87
|
+
schema: AGENT_METADATA_SCHEMA,
|
|
88
|
+
...input,
|
|
89
|
+
};
|
|
90
|
+
if (meta.training) {
|
|
91
|
+
meta.training = {
|
|
92
|
+
verified: false,
|
|
93
|
+
attestations: [],
|
|
94
|
+
...meta.training,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (meta.security) {
|
|
98
|
+
meta.security = {
|
|
99
|
+
attestedSecurityPolicy: false,
|
|
100
|
+
...meta.security,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return meta;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function encodeMetadata(metadata: AgentMetadata): string {
|
|
107
|
+
return JSON.stringify(metadata, null, 2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveIpfsUri(uri: string): string {
|
|
111
|
+
if (uri.startsWith("ipfs://")) {
|
|
112
|
+
return `https://ipfs.io/ipfs/${uri.slice(7)}`;
|
|
113
|
+
}
|
|
114
|
+
return uri;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function decodeMetadata(uri: string): Promise<AgentMetadata> {
|
|
118
|
+
if (uri.startsWith("data:")) {
|
|
119
|
+
// data:application/json;base64,<b64> or data:application/json,<encoded>
|
|
120
|
+
const commaIdx = uri.indexOf(",");
|
|
121
|
+
if (commaIdx === -1) throw new Error("malformed data URI");
|
|
122
|
+
const header = uri.slice(5, commaIdx);
|
|
123
|
+
const payload = uri.slice(commaIdx + 1);
|
|
124
|
+
const isBase64 = header.includes(";base64");
|
|
125
|
+
const json = isBase64 ? Buffer.from(payload, "base64").toString("utf-8") : decodeURIComponent(payload);
|
|
126
|
+
return validateMetadata(JSON.parse(json));
|
|
127
|
+
}
|
|
128
|
+
const url = resolveIpfsUri(uri);
|
|
129
|
+
const res = await fetch(url);
|
|
130
|
+
if (!res.ok) throw new Error(`failed to fetch metadata from ${url}: ${res.status} ${res.statusText}`);
|
|
131
|
+
const json = await res.json();
|
|
132
|
+
return validateMetadata(json);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function uploadMetadata(metadata: AgentMetadata, pinataJwt?: string): Promise<string> {
|
|
136
|
+
if (pinataJwt) {
|
|
137
|
+
const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
"Authorization": `Bearer ${pinataJwt}`,
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
pinataContent: metadata,
|
|
145
|
+
pinataMetadata: { name: `arc402-agent-metadata-${metadata.name ?? "unknown"}` },
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
const text = await res.text();
|
|
150
|
+
throw new Error(`Pinata upload failed: ${res.status} ${text}`);
|
|
151
|
+
}
|
|
152
|
+
const data = await res.json() as { IpfsHash: string };
|
|
153
|
+
return `ipfs://${data.IpfsHash}`;
|
|
154
|
+
}
|
|
155
|
+
// Fallback: data URI (no IPFS credentials configured)
|
|
156
|
+
const b64 = Buffer.from(encodeMetadata(metadata), "utf-8").toString("base64");
|
|
157
|
+
return `data:application/json;base64,${b64}`;
|
|
158
|
+
}
|
package/src/migration.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ContractRunner, ethers } from "ethers";
|
|
2
|
+
|
|
3
|
+
const MIGRATION_REGISTRY_ABI = [
|
|
4
|
+
"function registerMigration(address oldWallet, address newWallet) external",
|
|
5
|
+
"function resolveActiveWallet(address wallet) external view returns (address)",
|
|
6
|
+
"function getLineage(address wallet) external view returns (address[])",
|
|
7
|
+
"function approveMigrationTarget(address implementation) external",
|
|
8
|
+
"function migratedTo(address wallet) external view returns (address)",
|
|
9
|
+
"function migratedFrom(address wallet) external view returns (address)",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export class MigrationClient {
|
|
13
|
+
private address: string;
|
|
14
|
+
private runner?: ContractRunner;
|
|
15
|
+
|
|
16
|
+
constructor(address: string, runner?: ContractRunner) {
|
|
17
|
+
this.address = address;
|
|
18
|
+
this.runner = runner;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async registerMigration(oldWallet: string, newWallet: string, signer: ContractRunner): Promise<ethers.TransactionReceipt> {
|
|
22
|
+
const contract = new ethers.Contract(this.address, MIGRATION_REGISTRY_ABI, signer);
|
|
23
|
+
const tx = await contract.registerMigration(oldWallet, newWallet);
|
|
24
|
+
return tx.wait();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async resolveActiveWallet(wallet: string): Promise<string> {
|
|
28
|
+
const contract = new ethers.Contract(this.address, MIGRATION_REGISTRY_ABI, this.runner);
|
|
29
|
+
return contract.resolveActiveWallet(wallet);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getLineage(wallet: string): Promise<string[]> {
|
|
33
|
+
const contract = new ethers.Contract(this.address, MIGRATION_REGISTRY_ABI, this.runner);
|
|
34
|
+
const result = await contract.getLineage(wallet);
|
|
35
|
+
return [...result];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async approveMigrationTarget(impl: string, signer: ContractRunner): Promise<ethers.TransactionReceipt> {
|
|
39
|
+
const contract = new ethers.Contract(this.address, MIGRATION_REGISTRY_ABI, signer);
|
|
40
|
+
const tx = await contract.approveMigrationTarget(impl);
|
|
41
|
+
return tx.wait();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { ContractRunner, ethers } from "ethers";
|
|
2
|
+
import { AGREEMENT_TREE_ABI } from "./contracts";
|
|
3
|
+
import { ServiceAgreementClient } from "./agreement";
|
|
4
|
+
import { ProposeParams } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface AgreementTree {
|
|
7
|
+
root: bigint;
|
|
8
|
+
path: bigint[];
|
|
9
|
+
children: bigint[];
|
|
10
|
+
depth: number;
|
|
11
|
+
allSettled: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PaymentSplit {
|
|
15
|
+
address: string;
|
|
16
|
+
percentage: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PaymentDistribution {
|
|
20
|
+
total: bigint;
|
|
21
|
+
distributions: Array<{ address: string; amount: bigint; percentage: number }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CreateAgreementResult {
|
|
25
|
+
rootAgreementId: bigint;
|
|
26
|
+
childAgreementIds: bigint[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class AgreementTreeClient {
|
|
30
|
+
private treeContract: ethers.Contract;
|
|
31
|
+
private agreementClient: ServiceAgreementClient;
|
|
32
|
+
|
|
33
|
+
constructor(treeAddress: string, agreementAddress: string, runner: ContractRunner) {
|
|
34
|
+
this.treeContract = new ethers.Contract(treeAddress, AGREEMENT_TREE_ABI, runner);
|
|
35
|
+
this.agreementClient = new ServiceAgreementClient(agreementAddress, runner);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async registerSubAgreement(params: {
|
|
39
|
+
parentAgreementId: bigint;
|
|
40
|
+
childAgreementId: bigint;
|
|
41
|
+
}): Promise<ethers.TransactionReceipt> {
|
|
42
|
+
const tx = await this.treeContract.registerSubAgreement(
|
|
43
|
+
params.parentAgreementId,
|
|
44
|
+
params.childAgreementId,
|
|
45
|
+
);
|
|
46
|
+
return tx.wait();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Propose the root agreement, then propose each child agreement and register
|
|
51
|
+
* them all under the root in the AgreementTree contract.
|
|
52
|
+
*
|
|
53
|
+
* The caller must be the client for the root agreement AND the provider for
|
|
54
|
+
* all child agreements (i.e. the middle-layer agent that subcontracts work).
|
|
55
|
+
*/
|
|
56
|
+
async createAgreement(
|
|
57
|
+
rootTerms: ProposeParams,
|
|
58
|
+
childAgreements: ProposeParams[],
|
|
59
|
+
): Promise<CreateAgreementResult> {
|
|
60
|
+
const { agreementId: rootAgreementId } = await this.agreementClient.propose(rootTerms);
|
|
61
|
+
|
|
62
|
+
const childAgreementIds: bigint[] = [];
|
|
63
|
+
for (const child of childAgreements) {
|
|
64
|
+
const { agreementId: childId } = await this.agreementClient.propose(child);
|
|
65
|
+
await this.registerSubAgreement({
|
|
66
|
+
parentAgreementId: rootAgreementId,
|
|
67
|
+
childAgreementId: childId,
|
|
68
|
+
});
|
|
69
|
+
childAgreementIds.push(childId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { rootAgreementId, childAgreementIds };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getAgreementTree(rootAgreementId: bigint): Promise<AgreementTree> {
|
|
76
|
+
const id = BigInt(rootAgreementId);
|
|
77
|
+
const [root, path, children, depth, allSettled] = await Promise.all([
|
|
78
|
+
this.treeContract.getRoot(id),
|
|
79
|
+
this.treeContract.getPath(id),
|
|
80
|
+
this.treeContract.getChildren(id),
|
|
81
|
+
this.treeContract.getDepth(id),
|
|
82
|
+
this.treeContract.allChildrenSettled(id),
|
|
83
|
+
]);
|
|
84
|
+
return {
|
|
85
|
+
root: BigInt(root),
|
|
86
|
+
path: (path as bigint[]).map(BigInt),
|
|
87
|
+
children: (children as bigint[]).map(BigInt),
|
|
88
|
+
depth: Number(depth),
|
|
89
|
+
allSettled: Boolean(allSettled),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Alias for getAgreementTree — matches spec SDK interface. */
|
|
94
|
+
async getTree(agreementId: bigint): Promise<AgreementTree> {
|
|
95
|
+
return this.getAgreementTree(agreementId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async treeStatus(agreementId: bigint): Promise<{ allSettled: boolean; childCount: number }> {
|
|
99
|
+
const [allSettled, children] = await Promise.all([
|
|
100
|
+
this.treeContract.allChildrenSettled(BigInt(agreementId)),
|
|
101
|
+
this.treeContract.getChildren(BigInt(agreementId)),
|
|
102
|
+
]);
|
|
103
|
+
return {
|
|
104
|
+
allSettled: Boolean(allSettled),
|
|
105
|
+
childCount: (children as bigint[]).length,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Off-chain payment distribution helper.
|
|
111
|
+
* Splits paymentAmount across recipients according to their percentage shares.
|
|
112
|
+
* Percentages must sum to 100. This does not touch the chain.
|
|
113
|
+
*/
|
|
114
|
+
splitPayment(
|
|
115
|
+
paymentAmount: bigint,
|
|
116
|
+
splits: PaymentSplit[],
|
|
117
|
+
): PaymentDistribution {
|
|
118
|
+
const totalPct = splits.reduce((sum, s) => sum + s.percentage, 0);
|
|
119
|
+
if (Math.abs(totalPct - 100) > 0.001) {
|
|
120
|
+
throw new Error(`Split percentages must sum to 100 (got ${totalPct})`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const distributions = splits.map((s) => ({
|
|
124
|
+
address: s.address,
|
|
125
|
+
percentage: s.percentage,
|
|
126
|
+
// Use integer basis points (x100) to avoid floating-point in BigInt arithmetic
|
|
127
|
+
amount: (paymentAmount * BigInt(Math.round(s.percentage * 100))) / 10000n,
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
return { total: paymentAmount, distributions };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import type { ContractRunner } from "ethers";
|
|
3
|
+
import type { NegotiationMessage, NegotiationVerificationResult } from "./types";
|
|
4
|
+
import { computeMessageDigest } from "./negotiation";
|
|
5
|
+
|
|
6
|
+
const AGENT_REGISTRY_ABI = [
|
|
7
|
+
"function isRegistered(address wallet) view returns (bool)",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export interface NegotiationGuardOptions {
|
|
11
|
+
agentRegistryAddress: string;
|
|
12
|
+
runner: ContractRunner;
|
|
13
|
+
timestampToleranceSeconds?: number; // default 60
|
|
14
|
+
nonceCacheTtlMs?: number; // default 24h
|
|
15
|
+
maxMessageBytes?: number; // default 64KB
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class NegotiationGuard {
|
|
19
|
+
private registry: ethers.Contract;
|
|
20
|
+
private nonceCache = new Map<string, number>(); // nonceKey -> expiry timestamp (ms)
|
|
21
|
+
private opts: Required<NegotiationGuardOptions>;
|
|
22
|
+
|
|
23
|
+
constructor(options: NegotiationGuardOptions) {
|
|
24
|
+
this.opts = {
|
|
25
|
+
timestampToleranceSeconds: 60,
|
|
26
|
+
nonceCacheTtlMs: 24 * 60 * 60 * 1000,
|
|
27
|
+
maxMessageBytes: 64 * 1024,
|
|
28
|
+
...options,
|
|
29
|
+
};
|
|
30
|
+
this.registry = new ethers.Contract(
|
|
31
|
+
options.agentRegistryAddress,
|
|
32
|
+
AGENT_REGISTRY_ABI,
|
|
33
|
+
options.runner
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async verify(rawJson: string): Promise<NegotiationVerificationResult> {
|
|
38
|
+
// 1. Size check
|
|
39
|
+
if (Buffer.byteLength(rawJson, "utf8") > this.opts.maxMessageBytes) {
|
|
40
|
+
return { valid: false, error: "MESSAGE_TOO_LARGE" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Parse
|
|
44
|
+
let message: NegotiationMessage;
|
|
45
|
+
try {
|
|
46
|
+
message = JSON.parse(rawJson);
|
|
47
|
+
} catch {
|
|
48
|
+
return { valid: false, error: "SCHEMA_INVALID" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Required fields
|
|
52
|
+
if (!message.sig || message.timestamp == null || !message.from) {
|
|
53
|
+
return { valid: false, error: "SCHEMA_INVALID" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. Timestamp check
|
|
57
|
+
const now = Math.floor(Date.now() / 1000);
|
|
58
|
+
const delta = now - message.timestamp;
|
|
59
|
+
if (Math.abs(delta) > this.opts.timestampToleranceSeconds) {
|
|
60
|
+
return { valid: false, error: delta < 0 ? "TIMESTAMP_IN_FUTURE" : "TIMESTAMP_TOO_OLD" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 5. Expiry check (PROPOSE only)
|
|
64
|
+
if (message.type === "PROPOSE" && message.expiresAt != null && now > message.expiresAt) {
|
|
65
|
+
return { valid: false, error: "MESSAGE_EXPIRED" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 6. Signature recovery
|
|
69
|
+
let recoveredSigner: string;
|
|
70
|
+
try {
|
|
71
|
+
const digest = computeMessageDigest(message);
|
|
72
|
+
recoveredSigner = ethers.recoverAddress(
|
|
73
|
+
ethers.hashMessage(ethers.getBytes(digest)),
|
|
74
|
+
message.sig
|
|
75
|
+
);
|
|
76
|
+
} catch {
|
|
77
|
+
return { valid: false, error: "INVALID_SIGNATURE" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 7. Signer must match from
|
|
81
|
+
if (recoveredSigner.toLowerCase() !== message.from.toLowerCase()) {
|
|
82
|
+
return { valid: false, error: "INVALID_SIGNATURE" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 8. Registry check (fail open on registry downtime)
|
|
86
|
+
try {
|
|
87
|
+
const registered = await this.registry.isRegistered(recoveredSigner);
|
|
88
|
+
if (!registered) {
|
|
89
|
+
return { valid: false, error: "SIGNER_NOT_REGISTERED" };
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
console.warn(
|
|
93
|
+
"NegotiationGuard: AgentRegistry check failed — proceeding with signature-only verification"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 9. Nonce replay check
|
|
98
|
+
const nonceVal =
|
|
99
|
+
"nonce" in message && message.nonce
|
|
100
|
+
? message.nonce
|
|
101
|
+
: "refNonce" in message && message.refNonce
|
|
102
|
+
? message.refNonce
|
|
103
|
+
: "";
|
|
104
|
+
const nonceKey = `${message.from.toLowerCase()}:${nonceVal}:${message.timestamp}`;
|
|
105
|
+
this.pruneNonceCache();
|
|
106
|
+
if (this.nonceCache.has(nonceKey)) {
|
|
107
|
+
return { valid: false, error: "NONCE_REPLAYED" };
|
|
108
|
+
}
|
|
109
|
+
this.nonceCache.set(nonceKey, Date.now() + this.opts.nonceCacheTtlMs);
|
|
110
|
+
|
|
111
|
+
return { valid: true, recoveredSigner: recoveredSigner as any };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private pruneNonceCache(): void {
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
for (const [key, expiry] of this.nonceCache) {
|
|
117
|
+
if (now > expiry) this.nonceCache.delete(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Reset nonce cache (testing / restart) */
|
|
122
|
+
clearNonceCache(): void {
|
|
123
|
+
this.nonceCache.clear();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import type {
|
|
3
|
+
NegotiationMessage,
|
|
4
|
+
NegotiationProposal,
|
|
5
|
+
NegotiationCounter,
|
|
6
|
+
NegotiationAccept,
|
|
7
|
+
NegotiationReject,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
const MAX_MESSAGE_BYTES = 64 * 1024; // 64KB
|
|
11
|
+
|
|
12
|
+
// --- Digest computation ---
|
|
13
|
+
// digest = keccak256(abi.encodePacked(type, from, to, nonce, timestamp))
|
|
14
|
+
// nonce is message.nonce (PROPOSE) or message.refNonce (COUNTER/ACCEPT/REJECT) or bytes32(0) if absent
|
|
15
|
+
export function computeMessageDigest(message: NegotiationMessage | (Omit<NegotiationMessage, "sig"> & { sig?: string })): string {
|
|
16
|
+
const m = message as any;
|
|
17
|
+
const rawNonce: string = m.nonce || m.refNonce || ethers.ZeroHash;
|
|
18
|
+
// solidityPackedKeccak256 requires exactly 32 bytes for bytes32 — pad to 32 bytes
|
|
19
|
+
const nonce = ethers.zeroPadValue(rawNonce, 32);
|
|
20
|
+
|
|
21
|
+
return ethers.solidityPackedKeccak256(
|
|
22
|
+
["string", "address", "address", "bytes32", "uint256"],
|
|
23
|
+
[message.type, message.from, message.to, nonce, message.timestamp]
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Signing ---
|
|
28
|
+
|
|
29
|
+
export async function signNegotiationMessage<T extends NegotiationMessage>(
|
|
30
|
+
message: Omit<T, "sig">,
|
|
31
|
+
signer: ethers.Wallet | ethers.Signer
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
const digest = computeMessageDigest(message as any);
|
|
34
|
+
const sig = await signer.signMessage(ethers.getBytes(digest));
|
|
35
|
+
return { ...message, sig } as T;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Signed factory functions ---
|
|
39
|
+
|
|
40
|
+
export async function createSignedProposal(
|
|
41
|
+
input: Omit<NegotiationProposal, "type" | "nonce" | "sig" | "timestamp" | "expiresAt"> & {
|
|
42
|
+
nonce?: string;
|
|
43
|
+
expiresAt?: number;
|
|
44
|
+
},
|
|
45
|
+
signer: ethers.Wallet | ethers.Signer
|
|
46
|
+
): Promise<NegotiationProposal> {
|
|
47
|
+
const now = Math.floor(Date.now() / 1000);
|
|
48
|
+
const unsigned: Omit<NegotiationProposal, "sig"> = {
|
|
49
|
+
type: "PROPOSE",
|
|
50
|
+
nonce: input.nonce ?? ethers.hexlify(ethers.randomBytes(16)),
|
|
51
|
+
timestamp: now,
|
|
52
|
+
expiresAt: input.expiresAt ?? now + 3600,
|
|
53
|
+
...input,
|
|
54
|
+
};
|
|
55
|
+
return signNegotiationMessage(unsigned, signer);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function createSignedCounter(
|
|
59
|
+
input: Omit<NegotiationCounter, "type" | "sig" | "timestamp">,
|
|
60
|
+
signer: ethers.Wallet | ethers.Signer
|
|
61
|
+
): Promise<NegotiationCounter> {
|
|
62
|
+
const unsigned: Omit<NegotiationCounter, "sig"> = {
|
|
63
|
+
type: "COUNTER",
|
|
64
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
65
|
+
...input,
|
|
66
|
+
};
|
|
67
|
+
return signNegotiationMessage(unsigned, signer);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function createSignedAccept(
|
|
71
|
+
input: Omit<NegotiationAccept, "type" | "sig" | "timestamp">,
|
|
72
|
+
signer: ethers.Wallet | ethers.Signer
|
|
73
|
+
): Promise<NegotiationAccept> {
|
|
74
|
+
const unsigned: Omit<NegotiationAccept, "sig"> = {
|
|
75
|
+
type: "ACCEPT",
|
|
76
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
77
|
+
...input,
|
|
78
|
+
};
|
|
79
|
+
return signNegotiationMessage(unsigned, signer);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function createSignedReject(
|
|
83
|
+
input: Omit<NegotiationReject, "type" | "sig" | "timestamp">,
|
|
84
|
+
signer: ethers.Wallet | ethers.Signer
|
|
85
|
+
): Promise<NegotiationReject> {
|
|
86
|
+
const unsigned: Omit<NegotiationReject, "sig"> = {
|
|
87
|
+
type: "REJECT",
|
|
88
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
89
|
+
...input,
|
|
90
|
+
};
|
|
91
|
+
return signNegotiationMessage(unsigned, signer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Deprecated unsigned factories (backwards compat) ---
|
|
95
|
+
|
|
96
|
+
/** @deprecated Use createSignedProposal instead */
|
|
97
|
+
export function createNegotiationProposal(
|
|
98
|
+
input: Omit<NegotiationProposal, "type" | "nonce" | "sig" | "timestamp" | "expiresAt"> & { nonce?: string }
|
|
99
|
+
): NegotiationProposal {
|
|
100
|
+
console.warn("createNegotiationProposal: unsigned messages are deprecated. Use createSignedProposal instead.");
|
|
101
|
+
const now = Math.floor(Date.now() / 1000);
|
|
102
|
+
return {
|
|
103
|
+
type: "PROPOSE",
|
|
104
|
+
nonce: input.nonce ?? ethers.hexlify(ethers.randomBytes(16)),
|
|
105
|
+
timestamp: now,
|
|
106
|
+
expiresAt: now + 3600,
|
|
107
|
+
sig: "0x",
|
|
108
|
+
...input,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @deprecated Use createSignedCounter instead */
|
|
113
|
+
export function createNegotiationCounter(input: Omit<NegotiationCounter, "type" | "sig" | "timestamp">): NegotiationCounter {
|
|
114
|
+
console.warn("createNegotiationCounter: unsigned messages are deprecated. Use createSignedCounter instead.");
|
|
115
|
+
return { type: "COUNTER", timestamp: Math.floor(Date.now() / 1000), sig: "0x", ...input };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @deprecated Use createSignedAccept instead */
|
|
119
|
+
export function createNegotiationAccept(input: Omit<NegotiationAccept, "type" | "sig" | "timestamp">): NegotiationAccept {
|
|
120
|
+
console.warn("createNegotiationAccept: unsigned messages are deprecated. Use createSignedAccept instead.");
|
|
121
|
+
return { type: "ACCEPT", timestamp: Math.floor(Date.now() / 1000), sig: "0x", ...input };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @deprecated Use createSignedReject instead */
|
|
125
|
+
export function createNegotiationReject(input: Omit<NegotiationReject, "type" | "sig" | "timestamp">): NegotiationReject {
|
|
126
|
+
console.warn("createNegotiationReject: unsigned messages are deprecated. Use createSignedReject instead.");
|
|
127
|
+
return { type: "REJECT", timestamp: Math.floor(Date.now() / 1000), sig: "0x", ...input };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parseNegotiationMessage(json: string): NegotiationMessage {
|
|
131
|
+
if (Buffer.byteLength(json, "utf8") > MAX_MESSAGE_BYTES) {
|
|
132
|
+
throw new Error("NegotiationMessage exceeds 64KB size limit");
|
|
133
|
+
}
|
|
134
|
+
return JSON.parse(json) as NegotiationMessage;
|
|
135
|
+
}
|