@lawreneliang/atel-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/dist/anchor/base.d.ts +21 -0
- package/dist/anchor/base.js +26 -0
- package/dist/anchor/bsc.d.ts +20 -0
- package/dist/anchor/bsc.js +25 -0
- package/dist/anchor/evm.d.ts +67 -0
- package/dist/anchor/evm.js +176 -0
- package/dist/anchor/index.d.ts +173 -0
- package/dist/anchor/index.js +165 -0
- package/dist/anchor/mock.d.ts +43 -0
- package/dist/anchor/mock.js +100 -0
- package/dist/anchor/solana.d.ts +62 -0
- package/dist/anchor/solana.js +190 -0
- package/dist/gateway/index.d.ts +278 -0
- package/dist/gateway/index.js +520 -0
- package/dist/graph/index.d.ts +215 -0
- package/dist/graph/index.js +524 -0
- package/dist/identity/index.d.ts +114 -0
- package/dist/identity/index.js +178 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +16 -0
- package/dist/orchestrator/index.d.ts +190 -0
- package/dist/orchestrator/index.js +297 -0
- package/dist/policy/index.d.ts +100 -0
- package/dist/policy/index.js +206 -0
- package/dist/proof/index.d.ts +220 -0
- package/dist/proof/index.js +541 -0
- package/dist/rollback/index.d.ts +76 -0
- package/dist/rollback/index.js +91 -0
- package/dist/schema/capability-schema.json +52 -0
- package/dist/schema/index.d.ts +128 -0
- package/dist/schema/index.js +163 -0
- package/dist/schema/task-schema.json +69 -0
- package/dist/score/index.d.ts +132 -0
- package/dist/score/index.js +202 -0
- package/dist/service/index.d.ts +34 -0
- package/dist/service/index.js +273 -0
- package/dist/service/server.d.ts +7 -0
- package/dist/service/server.js +22 -0
- package/dist/trace/index.d.ts +217 -0
- package/dist/trace/index.js +446 -0
- package/dist/trust/index.d.ts +84 -0
- package/dist/trust/index.js +107 -0
- package/dist/trust-sync/index.d.ts +30 -0
- package/dist/trust-sync/index.js +57 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ATEL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ATEL SDK
|
|
2
|
+
|
|
3
|
+
**Agent Trust & Economics Layer** — a TypeScript protocol SDK for building trustworthy, auditable multi-agent systems.
|
|
4
|
+
|
|
5
|
+
ATEL provides the cryptographic primitives and protocol building blocks that let AI agents collaborate safely: identity verification, scoped consent, policy enforcement, tamper-evident execution traces, Merkle-tree proofs, and reputation scoring.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install dependencies
|
|
11
|
+
npm install
|
|
12
|
+
|
|
13
|
+
# Build
|
|
14
|
+
npm run build
|
|
15
|
+
|
|
16
|
+
# Run the end-to-end demo
|
|
17
|
+
npx tsx demo/full-demo.ts
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Phase 0.5 commands:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Real external API e2e demo
|
|
24
|
+
npm run demo:real
|
|
25
|
+
|
|
26
|
+
# Testnet anchoring smoke (requires chain env vars)
|
|
27
|
+
npm run smoke:anchor
|
|
28
|
+
|
|
29
|
+
# Performance baseline
|
|
30
|
+
npm run test:perf
|
|
31
|
+
|
|
32
|
+
# 5-10 agent continuous run (Phase 0.5)
|
|
33
|
+
npm run run:cluster
|
|
34
|
+
|
|
35
|
+
# deploy acceptance (service + sync + proof + anchor)
|
|
36
|
+
npm run acceptance:deploy
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The demo simulates two agents collaborating on a flight search task, exercising the end-to-end trust workflow.
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
ATEL is organized into 13 composable modules:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
47
|
+
│ ATEL SDK │
|
|
48
|
+
├──────────┬──────────┬──────────┬──────────┬─────────────────┤
|
|
49
|
+
│ Identity │ Schema │ Policy │ Gateway │ Trace │
|
|
50
|
+
├──────────┴──────────┴──────────┴──────────┴─────────────────┤
|
|
51
|
+
│ Proof │ Score │ Graph │ TrustManager │ Rollback │ Anchor │
|
|
52
|
+
├───────────────────────────────┬──────────────────────────────┤
|
|
53
|
+
│ Orchestrator │ Trust Score Service │
|
|
54
|
+
└───────────────────────────────┴──────────────────────────────┘
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Module | Description |
|
|
58
|
+
|--------|-------------|
|
|
59
|
+
| **Identity** | Ed25519 key pairs, DID creation (`did:atel:*`), signing & verification |
|
|
60
|
+
| **Schema** | Task and Capability JSON schemas, validation, factory functions, matching |
|
|
61
|
+
| **Policy** | Consent tokens (scoped, time-limited), policy engine with call tracking |
|
|
62
|
+
| **Gateway** | Central tool invocation gateway with policy enforcement and deterministic hashing |
|
|
63
|
+
| **Trace** | Append-only, hash-chained execution log with auto-checkpoints |
|
|
64
|
+
| **Proof** | Merkle-tree proof bundles with multi-check verification |
|
|
65
|
+
| **Score** | Local trust-score computation based on execution history |
|
|
66
|
+
| **Graph** | Multi-dimensional trust graph, direct/indirect/composite trust |
|
|
67
|
+
| **TrustManager** | Unified score + graph submission and trust query API |
|
|
68
|
+
| **Rollback** | Compensation and rollback execution manager |
|
|
69
|
+
| **Anchor** | Multi-chain proof anchoring (Base/BSC/Solana/Mock) |
|
|
70
|
+
| **Orchestrator** | High-level API wiring task delegation/execution/verify |
|
|
71
|
+
| **Service** | HTTP API for score + graph queries with JSON persistence |
|
|
72
|
+
|
|
73
|
+
## Trust Modes
|
|
74
|
+
|
|
75
|
+
ATEL supports two deployment modes that can coexist:
|
|
76
|
+
|
|
77
|
+
- `Local Mode` (default): execute, prove, verify, and score locally with no network dependency.
|
|
78
|
+
- `Network Mode` (optional): keep local trust updates, and additionally sync summaries to a shared trust service.
|
|
79
|
+
|
|
80
|
+
This means local capability is never removed when network trust is enabled.
|
|
81
|
+
|
|
82
|
+
## API Reference
|
|
83
|
+
|
|
84
|
+
Detailed API guide: `docs/API.md`
|
|
85
|
+
Start here (one-page onboarding): `docs/START-HERE.md`
|
|
86
|
+
5-minute quickstart: `docs/QUICKSTART-5MIN.md`
|
|
87
|
+
Phase 0.5 runbook: `docs/PHASE-0.5.md`
|
|
88
|
+
Service deployment: `docs/SERVICE-DEPLOY.md`
|
|
89
|
+
|
|
90
|
+
ATEL skill package: `skills/atel/SKILL.md`
|
|
91
|
+
|
|
92
|
+
### Module 1: Identity
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { AgentIdentity, generateKeyPair, createDID, sign, verify } from '@lawreneliang/atel-sdk';
|
|
96
|
+
|
|
97
|
+
const agent = new AgentIdentity();
|
|
98
|
+
console.log(agent.did); // "did:atel:..."
|
|
99
|
+
const sig = agent.sign(payload);
|
|
100
|
+
const ok = agent.verify(payload, sig);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Module 2: Schema
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { createTask, createCapability, matchTaskToCapability } from '@lawreneliang/atel-sdk';
|
|
107
|
+
|
|
108
|
+
const task = createTask({
|
|
109
|
+
issuer: agent.did,
|
|
110
|
+
intent: { type: 'flight_search', goal: 'Find flights SIN→HND' },
|
|
111
|
+
risk: { level: 'medium' },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const cap = createCapability({
|
|
115
|
+
provider: executor.did,
|
|
116
|
+
capabilities: [{ type: 'flight_search', description: '...' }],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const match = matchTaskToCapability(task, cap);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Module 3: Policy
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { mintConsentToken, verifyConsentToken, PolicyEngine } from '@lawreneliang/atel-sdk';
|
|
126
|
+
|
|
127
|
+
const token = mintConsentToken(
|
|
128
|
+
issuer.did, executor.did,
|
|
129
|
+
['tool:http:get', 'data:public_web:read'],
|
|
130
|
+
{ max_calls: 10, ttl_sec: 3600 },
|
|
131
|
+
'medium',
|
|
132
|
+
issuer.secretKey,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
verifyConsentToken(token, issuer.publicKey);
|
|
136
|
+
|
|
137
|
+
const engine = new PolicyEngine(token);
|
|
138
|
+
const decision = engine.evaluate(action); // 'allow' | 'deny' | 'needs_confirm'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Module 4: Gateway
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { ToolGateway } from '@lawreneliang/atel-sdk';
|
|
145
|
+
|
|
146
|
+
const gateway = new ToolGateway(policyEngine);
|
|
147
|
+
gateway.registerTool('http.get', async (input) => { /* ... */ });
|
|
148
|
+
|
|
149
|
+
const result = await gateway.callTool({
|
|
150
|
+
tool: 'http.get',
|
|
151
|
+
input: { url: '...' },
|
|
152
|
+
consentToken: '...',
|
|
153
|
+
});
|
|
154
|
+
// result.output, result.input_hash, result.output_hash
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Module 5: Trace
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { ExecutionTrace } from '@lawreneliang/atel-sdk';
|
|
161
|
+
|
|
162
|
+
const trace = new ExecutionTrace(taskId, agentIdentity);
|
|
163
|
+
trace.append('TASK_ACCEPTED', { ... });
|
|
164
|
+
trace.append('TOOL_CALL', { ... });
|
|
165
|
+
trace.finalize(result);
|
|
166
|
+
|
|
167
|
+
const { valid, errors } = trace.verify();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Module 6: Proof
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { ProofGenerator, ProofVerifier } from '@lawreneliang/atel-sdk';
|
|
174
|
+
|
|
175
|
+
const gen = new ProofGenerator(trace, identity);
|
|
176
|
+
const bundle = gen.generate(policyRef, consentRef, resultRef);
|
|
177
|
+
|
|
178
|
+
const report = ProofVerifier.verify(bundle, { trace });
|
|
179
|
+
// report.valid, report.checks, report.summary
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Module 7: Score
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { TrustScoreClient } from '@lawreneliang/atel-sdk';
|
|
186
|
+
|
|
187
|
+
const client = new TrustScoreClient();
|
|
188
|
+
client.submitExecutionSummary({ executor: did, task_id, success: true, ... });
|
|
189
|
+
|
|
190
|
+
const report = client.getAgentScore(did);
|
|
191
|
+
// report.trust_score (0-100), report.risk_flags, etc.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Trust Score Formula
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
base = success_rate × 60
|
|
198
|
+
volume = min(total_tasks / 100, 1) × 15
|
|
199
|
+
risk_bonus = (high_risk_successes / total) × 15
|
|
200
|
+
consistency = (1 − violation_rate) × 10
|
|
201
|
+
─────────────────────────────────────────
|
|
202
|
+
score = base + volume + risk_bonus + consistency (clamped 0–100)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Current Status (2026-02-13)
|
|
206
|
+
|
|
207
|
+
- [x] **Phase 0 MVP complete** — 13 modules implemented, core trust workflow end-to-end
|
|
208
|
+
- [x] **241 tests in suite** — unit/integration coverage across modules
|
|
209
|
+
- [x] **Demo coverage** — success path + failure scenarios
|
|
210
|
+
- [ ] **Phase 0.5 validation** — real agents + real external tools + real testnet anchoring
|
|
211
|
+
|
|
212
|
+
## Roadmap
|
|
213
|
+
|
|
214
|
+
- [ ] **Phase 0.5** — Internal multi-agent cluster with real API/tool workloads
|
|
215
|
+
- [ ] **Phase 1** — Enterprise pilot + external integration hardening
|
|
216
|
+
- [ ] **Phase 2** — Open SDK access + Trust Score/Graph network rollout
|
|
217
|
+
- [ ] **Phase 3+** — Discovery/Directory and Router/Marketplace layers
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Chain Anchor Provider.
|
|
3
|
+
*
|
|
4
|
+
* Extends {@link EvmAnchorProvider} with Base-specific defaults.
|
|
5
|
+
* Base is an EVM-compatible L2 built on the OP Stack.
|
|
6
|
+
*
|
|
7
|
+
* Default RPC: https://mainnet.base.org
|
|
8
|
+
*/
|
|
9
|
+
import { EvmAnchorProvider, type EvmAnchorConfig } from './evm.js';
|
|
10
|
+
/**
|
|
11
|
+
* Anchor provider for the Base chain.
|
|
12
|
+
*/
|
|
13
|
+
export declare class BaseAnchorProvider extends EvmAnchorProvider {
|
|
14
|
+
/** Default Base mainnet RPC URL */
|
|
15
|
+
static readonly DEFAULT_RPC_URL = "https://mainnet.base.org";
|
|
16
|
+
/**
|
|
17
|
+
* @param config - RPC URL and optional private key.
|
|
18
|
+
* If `rpcUrl` is omitted, the Base mainnet default is used.
|
|
19
|
+
*/
|
|
20
|
+
constructor(config?: Partial<EvmAnchorConfig>);
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Chain Anchor Provider.
|
|
3
|
+
*
|
|
4
|
+
* Extends {@link EvmAnchorProvider} with Base-specific defaults.
|
|
5
|
+
* Base is an EVM-compatible L2 built on the OP Stack.
|
|
6
|
+
*
|
|
7
|
+
* Default RPC: https://mainnet.base.org
|
|
8
|
+
*/
|
|
9
|
+
import { EvmAnchorProvider } from './evm.js';
|
|
10
|
+
/**
|
|
11
|
+
* Anchor provider for the Base chain.
|
|
12
|
+
*/
|
|
13
|
+
export class BaseAnchorProvider extends EvmAnchorProvider {
|
|
14
|
+
/** Default Base mainnet RPC URL */
|
|
15
|
+
static DEFAULT_RPC_URL = 'https://mainnet.base.org';
|
|
16
|
+
/**
|
|
17
|
+
* @param config - RPC URL and optional private key.
|
|
18
|
+
* If `rpcUrl` is omitted, the Base mainnet default is used.
|
|
19
|
+
*/
|
|
20
|
+
constructor(config) {
|
|
21
|
+
super('Base', 'base', {
|
|
22
|
+
rpcUrl: config?.rpcUrl ?? BaseAnchorProvider.DEFAULT_RPC_URL,
|
|
23
|
+
privateKey: config?.privateKey,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BSC (BNB Smart Chain) Anchor Provider.
|
|
3
|
+
*
|
|
4
|
+
* Extends {@link EvmAnchorProvider} with BSC-specific defaults.
|
|
5
|
+
*
|
|
6
|
+
* Default RPC: https://bsc-dataseed.binance.org
|
|
7
|
+
*/
|
|
8
|
+
import { EvmAnchorProvider, type EvmAnchorConfig } from './evm.js';
|
|
9
|
+
/**
|
|
10
|
+
* Anchor provider for the BSC chain.
|
|
11
|
+
*/
|
|
12
|
+
export declare class BSCAnchorProvider extends EvmAnchorProvider {
|
|
13
|
+
/** Default BSC mainnet RPC URL */
|
|
14
|
+
static readonly DEFAULT_RPC_URL = "https://bsc-dataseed.binance.org";
|
|
15
|
+
/**
|
|
16
|
+
* @param config - RPC URL and optional private key.
|
|
17
|
+
* If `rpcUrl` is omitted, the BSC mainnet default is used.
|
|
18
|
+
*/
|
|
19
|
+
constructor(config?: Partial<EvmAnchorConfig>);
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BSC (BNB Smart Chain) Anchor Provider.
|
|
3
|
+
*
|
|
4
|
+
* Extends {@link EvmAnchorProvider} with BSC-specific defaults.
|
|
5
|
+
*
|
|
6
|
+
* Default RPC: https://bsc-dataseed.binance.org
|
|
7
|
+
*/
|
|
8
|
+
import { EvmAnchorProvider } from './evm.js';
|
|
9
|
+
/**
|
|
10
|
+
* Anchor provider for the BSC chain.
|
|
11
|
+
*/
|
|
12
|
+
export class BSCAnchorProvider extends EvmAnchorProvider {
|
|
13
|
+
/** Default BSC mainnet RPC URL */
|
|
14
|
+
static DEFAULT_RPC_URL = 'https://bsc-dataseed.binance.org';
|
|
15
|
+
/**
|
|
16
|
+
* @param config - RPC URL and optional private key.
|
|
17
|
+
* If `rpcUrl` is omitted, the BSC mainnet default is used.
|
|
18
|
+
*/
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super('BSC', 'bsc', {
|
|
21
|
+
rpcUrl: config?.rpcUrl ?? BSCAnchorProvider.DEFAULT_RPC_URL,
|
|
22
|
+
privateKey: config?.privateKey,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVM Anchor Provider — shared base class for EVM-compatible chains.
|
|
3
|
+
*
|
|
4
|
+
* Anchors a hash by sending a zero-value transaction to the signer's
|
|
5
|
+
* own address with the hash encoded in the `data` field. This is the
|
|
6
|
+
* simplest on-chain timestamping approach — no contract deployment needed.
|
|
7
|
+
*
|
|
8
|
+
* Both {@link BaseAnchorProvider} and {@link BSCAnchorProvider} extend this class.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* ⚠️ SECURITY: The `privateKey` is used to sign transactions.
|
|
12
|
+
* Never hard-code private keys in source code. Use environment variables
|
|
13
|
+
* or a secure vault in production.
|
|
14
|
+
*/
|
|
15
|
+
import { ethers } from 'ethers';
|
|
16
|
+
import type { AnchorProvider, AnchorRecord, AnchorVerification } from './index.js';
|
|
17
|
+
/** Configuration for an EVM-based anchor provider */
|
|
18
|
+
export interface EvmAnchorConfig {
|
|
19
|
+
/** JSON-RPC endpoint URL */
|
|
20
|
+
rpcUrl: string;
|
|
21
|
+
/**
|
|
22
|
+
* Hex-encoded private key for signing transactions.
|
|
23
|
+
* Optional — if omitted the provider can only verify / lookup.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ SECURITY: Keep this value secret. Load from env vars or a vault.
|
|
26
|
+
*/
|
|
27
|
+
privateKey?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Abstract EVM anchor provider.
|
|
31
|
+
*
|
|
32
|
+
* Subclasses only need to supply `name`, `chain`, and a default RPC URL.
|
|
33
|
+
*/
|
|
34
|
+
export declare class EvmAnchorProvider implements AnchorProvider {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly chain: string;
|
|
37
|
+
/** Ethers JSON-RPC provider (read-only access) */
|
|
38
|
+
protected readonly provider: ethers.JsonRpcProvider;
|
|
39
|
+
/** Wallet for signing (undefined when no private key is supplied) */
|
|
40
|
+
protected readonly wallet?: ethers.Wallet;
|
|
41
|
+
/**
|
|
42
|
+
* @param name - Human-readable provider name.
|
|
43
|
+
* @param chain - Chain identifier.
|
|
44
|
+
* @param config - RPC URL and optional private key.
|
|
45
|
+
*/
|
|
46
|
+
constructor(name: string, chain: string, config: EvmAnchorConfig);
|
|
47
|
+
/**
|
|
48
|
+
* Encode a hash string into the hex data payload for a transaction.
|
|
49
|
+
*
|
|
50
|
+
* Format: `0x` + hex(ATEL_ANCHOR:<hash>)
|
|
51
|
+
*/
|
|
52
|
+
static encodeData(hash: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Decode the hash from a transaction data field.
|
|
55
|
+
*
|
|
56
|
+
* @returns The decoded hash, or `null` if the data doesn't match the expected format.
|
|
57
|
+
*/
|
|
58
|
+
static decodeData(data: string): string | null;
|
|
59
|
+
/** @inheritdoc */
|
|
60
|
+
anchor(hash: string, metadata?: Record<string, unknown>): Promise<AnchorRecord>;
|
|
61
|
+
/** @inheritdoc */
|
|
62
|
+
verify(hash: string, txHash: string): Promise<AnchorVerification>;
|
|
63
|
+
/** @inheritdoc */
|
|
64
|
+
lookup(hash: string): Promise<AnchorRecord[]>;
|
|
65
|
+
/** @inheritdoc */
|
|
66
|
+
isAvailable(): Promise<boolean>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVM Anchor Provider — shared base class for EVM-compatible chains.
|
|
3
|
+
*
|
|
4
|
+
* Anchors a hash by sending a zero-value transaction to the signer's
|
|
5
|
+
* own address with the hash encoded in the `data` field. This is the
|
|
6
|
+
* simplest on-chain timestamping approach — no contract deployment needed.
|
|
7
|
+
*
|
|
8
|
+
* Both {@link BaseAnchorProvider} and {@link BSCAnchorProvider} extend this class.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* ⚠️ SECURITY: The `privateKey` is used to sign transactions.
|
|
12
|
+
* Never hard-code private keys in source code. Use environment variables
|
|
13
|
+
* or a secure vault in production.
|
|
14
|
+
*/
|
|
15
|
+
import { ethers } from 'ethers';
|
|
16
|
+
/**
|
|
17
|
+
* Prefix prepended to the hash in the transaction data field
|
|
18
|
+
* so anchored transactions are easily identifiable.
|
|
19
|
+
*/
|
|
20
|
+
const ANCHOR_PREFIX = 'ATEL_ANCHOR:';
|
|
21
|
+
/**
|
|
22
|
+
* Abstract EVM anchor provider.
|
|
23
|
+
*
|
|
24
|
+
* Subclasses only need to supply `name`, `chain`, and a default RPC URL.
|
|
25
|
+
*/
|
|
26
|
+
export class EvmAnchorProvider {
|
|
27
|
+
name;
|
|
28
|
+
chain;
|
|
29
|
+
/** Ethers JSON-RPC provider (read-only access) */
|
|
30
|
+
provider;
|
|
31
|
+
/** Wallet for signing (undefined when no private key is supplied) */
|
|
32
|
+
wallet;
|
|
33
|
+
/**
|
|
34
|
+
* @param name - Human-readable provider name.
|
|
35
|
+
* @param chain - Chain identifier.
|
|
36
|
+
* @param config - RPC URL and optional private key.
|
|
37
|
+
*/
|
|
38
|
+
constructor(name, chain, config) {
|
|
39
|
+
this.name = name;
|
|
40
|
+
this.chain = chain;
|
|
41
|
+
this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
42
|
+
if (config.privateKey) {
|
|
43
|
+
// ⚠️ SECURITY: The wallet holds the private key in memory.
|
|
44
|
+
this.wallet = new ethers.Wallet(config.privateKey, this.provider);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Encode a hash string into the hex data payload for a transaction.
|
|
49
|
+
*
|
|
50
|
+
* Format: `0x` + hex(ATEL_ANCHOR:<hash>)
|
|
51
|
+
*/
|
|
52
|
+
static encodeData(hash) {
|
|
53
|
+
return ethers.hexlify(ethers.toUtf8Bytes(`${ANCHOR_PREFIX}${hash}`));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decode the hash from a transaction data field.
|
|
57
|
+
*
|
|
58
|
+
* @returns The decoded hash, or `null` if the data doesn't match the expected format.
|
|
59
|
+
*/
|
|
60
|
+
static decodeData(data) {
|
|
61
|
+
try {
|
|
62
|
+
const text = ethers.toUtf8String(data);
|
|
63
|
+
if (text.startsWith(ANCHOR_PREFIX)) {
|
|
64
|
+
return text.slice(ANCHOR_PREFIX.length);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** @inheritdoc */
|
|
73
|
+
async anchor(hash, metadata) {
|
|
74
|
+
if (!this.wallet) {
|
|
75
|
+
throw new Error(`${this.name}: Cannot anchor without a private key`);
|
|
76
|
+
}
|
|
77
|
+
const data = EvmAnchorProvider.encodeData(hash);
|
|
78
|
+
try {
|
|
79
|
+
const tx = await this.wallet.sendTransaction({
|
|
80
|
+
to: this.wallet.address,
|
|
81
|
+
value: 0n,
|
|
82
|
+
data,
|
|
83
|
+
});
|
|
84
|
+
const receipt = await tx.wait();
|
|
85
|
+
if (!receipt) {
|
|
86
|
+
throw new Error('Transaction receipt is null — tx may have been dropped');
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
hash,
|
|
90
|
+
txHash: receipt.hash,
|
|
91
|
+
chain: this.chain,
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
blockNumber: receipt.blockNumber,
|
|
94
|
+
metadata,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
throw new Error(`${this.name} anchor failed: ${message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** @inheritdoc */
|
|
103
|
+
async verify(hash, txHash) {
|
|
104
|
+
try {
|
|
105
|
+
const tx = await this.provider.getTransaction(txHash);
|
|
106
|
+
if (!tx) {
|
|
107
|
+
return {
|
|
108
|
+
valid: false,
|
|
109
|
+
hash,
|
|
110
|
+
txHash,
|
|
111
|
+
chain: this.chain,
|
|
112
|
+
detail: 'Transaction not found',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const decoded = EvmAnchorProvider.decodeData(tx.data);
|
|
116
|
+
if (decoded === null) {
|
|
117
|
+
return {
|
|
118
|
+
valid: false,
|
|
119
|
+
hash,
|
|
120
|
+
txHash,
|
|
121
|
+
chain: this.chain,
|
|
122
|
+
detail: 'Transaction data does not contain a valid anchor',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const valid = decoded === hash;
|
|
126
|
+
// Try to get block timestamp
|
|
127
|
+
let blockTimestamp;
|
|
128
|
+
if (tx.blockNumber) {
|
|
129
|
+
try {
|
|
130
|
+
const block = await this.provider.getBlock(tx.blockNumber);
|
|
131
|
+
blockTimestamp = block ? block.timestamp * 1000 : undefined;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Non-critical — skip timestamp
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
valid,
|
|
139
|
+
hash,
|
|
140
|
+
txHash,
|
|
141
|
+
chain: this.chain,
|
|
142
|
+
blockTimestamp,
|
|
143
|
+
detail: valid
|
|
144
|
+
? 'Hash matches on-chain data'
|
|
145
|
+
: `Hash mismatch: expected "${hash}", found "${decoded}"`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
150
|
+
return {
|
|
151
|
+
valid: false,
|
|
152
|
+
hash,
|
|
153
|
+
txHash,
|
|
154
|
+
chain: this.chain,
|
|
155
|
+
detail: `Verification error: ${message}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** @inheritdoc */
|
|
160
|
+
async lookup(hash) {
|
|
161
|
+
// On-chain lookup without an indexer is not feasible for EVM chains.
|
|
162
|
+
// In production, integrate with a subgraph or event indexer.
|
|
163
|
+
// For now, return an empty array — local records are managed by AnchorManager.
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
/** @inheritdoc */
|
|
167
|
+
async isAvailable() {
|
|
168
|
+
try {
|
|
169
|
+
await this.provider.getBlockNumber();
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|