clawsats-indelible 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 ADDED
@@ -0,0 +1,147 @@
1
+ # clawsats-indelible
2
+
3
+ Persistent blockchain memory for ClawSats AI agents — powered by [Indelible](https://indelible.one).
4
+
5
+ ```
6
+ npm install clawsats-indelible
7
+ ```
8
+
9
+ ## What's Included
10
+
11
+ Six BRC standards, ready to use:
12
+
13
+ | Standard | What it does | Module |
14
+ |----------|-------------|--------|
15
+ | **BRC-105** | Payment verification middleware | `middleware.js` |
16
+ | **BRC-52** | Agent identity certificates | `identity.js` |
17
+ | **BRC-31** | Mutual authentication (Authrite) | `auth.js` |
18
+ | **BRC-77** | Cryptographically signed actions | `signing.js` |
19
+ | **BRC-78** | Encrypted agent-to-agent messaging | `encryption.js` |
20
+ | **SHIP/SLAP** | Service discovery & advertising | `discovery.js` |
21
+
22
+ ## Quick Start
23
+
24
+ ### Save & Load Agent Memory
25
+
26
+ ```js
27
+ import { IndelibleMemoryBridge } from 'clawsats-indelible/bridge'
28
+
29
+ const bridge = new IndelibleMemoryBridge({
30
+ indelibleUrl: 'https://indelible.one',
31
+ operatorAddress: 'YOUR_OPERATOR_ADDRESS',
32
+ agentAddress: 'YOUR_AGENT_ADDRESS'
33
+ })
34
+
35
+ // Save conversation to blockchain
36
+ const result = await bridge.save('my-agent', messages, {
37
+ summary: 'Customer support session'
38
+ })
39
+
40
+ // Load previous context
41
+ const context = await bridge.load('my-agent', { numSessions: 3 })
42
+ ```
43
+
44
+ ### Agent Identity (BRC-52)
45
+
46
+ ```js
47
+ import { createAgentCertificate, verifyAgentCertificate } from 'clawsats-indelible/identity'
48
+
49
+ const cert = await createAgentCertificate({
50
+ operatorWif: 'OPERATOR_PRIVATE_KEY',
51
+ agentPubKey: 'AGENT_PUBLIC_KEY',
52
+ agentName: 'MyAgent',
53
+ capabilities: ['save_context', 'load_context']
54
+ })
55
+
56
+ const verified = await verifyAgentCertificate(cert.serialized)
57
+ // { valid: true, fields: { agentName: 'MyAgent', capabilities: [...] } }
58
+ ```
59
+
60
+ ### Signed Actions (BRC-77)
61
+
62
+ ```js
63
+ import { signAction, verifyAction } from 'clawsats-indelible/signing'
64
+
65
+ const signed = signAction({
66
+ privateKeyWif: 'AGENT_PRIVATE_KEY',
67
+ action: 'save_context',
68
+ payload: { summary: 'Session data', messageCount: 5 }
69
+ })
70
+
71
+ const valid = verifyAction({
72
+ signature: signed.signature,
73
+ action: signed.action,
74
+ timestamp: signed.timestamp,
75
+ payload: { summary: 'Session data', messageCount: 5 }
76
+ })
77
+ ```
78
+
79
+ ### Encrypted Messaging (BRC-78)
80
+
81
+ ```js
82
+ import { encryptMessage, decryptMessage } from 'clawsats-indelible/encryption'
83
+
84
+ // Agent 1 encrypts
85
+ const encrypted = encryptMessage({
86
+ senderWif: 'SENDER_PRIVATE_KEY',
87
+ recipientPubKey: 'RECIPIENT_PUBLIC_KEY',
88
+ message: { context: 'handoff data', sessionId: 'abc123' }
89
+ })
90
+
91
+ // Agent 2 decrypts
92
+ const decrypted = decryptMessage({
93
+ recipientWif: 'RECIPIENT_PRIVATE_KEY',
94
+ encryptedHex: encrypted,
95
+ parseJson: true
96
+ })
97
+ ```
98
+
99
+ ### Service Discovery (SHIP/SLAP)
100
+
101
+ ```js
102
+ import { createServiceBroadcaster, createServiceResolver } from 'clawsats-indelible/discovery'
103
+
104
+ // Advertise capabilities
105
+ const { broadcaster, topics } = createServiceBroadcaster({
106
+ capabilities: ['save_context', 'load_context']
107
+ })
108
+
109
+ // Discover agents
110
+ const { resolver, lookup } = createServiceResolver()
111
+ const results = await lookup('save_context')
112
+ ```
113
+
114
+ ### Payment Middleware (BRC-105)
115
+
116
+ ```js
117
+ import { createIndeliblePaymentMiddleware } from 'clawsats-indelible/middleware'
118
+
119
+ const paywall = createIndeliblePaymentMiddleware({
120
+ operatorAddress: 'YOUR_ADDRESS',
121
+ price: 15
122
+ })
123
+
124
+ app.post('/api/save', paywall, (req, res) => {
125
+ // req.payment contains verified tx details
126
+ })
127
+ ```
128
+
129
+ ## Pricing
130
+
131
+ | Action | Cost |
132
+ |--------|------|
133
+ | `save_context` | 15 sats |
134
+ | `load_context` | 10 sats |
135
+ | Protocol fee | 2 sats |
136
+
137
+ ## Example
138
+
139
+ Run the full demo showing all 6 BRC standards:
140
+
141
+ ```
142
+ node examples/agent-demo.js [indelible-url]
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClawSats + Indelible Agent Demo
4
+ *
5
+ * A complete working example of an AI agent that:
6
+ * 1. Generates its own identity (keys + certificate)
7
+ * 2. Registers with the Indelible server
8
+ * 3. Signs its actions cryptographically (BRC-77)
9
+ * 4. Saves memory to the blockchain
10
+ * 5. Loads memory back from the blockchain
11
+ * 6. Sends encrypted messages to another agent (BRC-78)
12
+ * 7. Advertises its capabilities (SHIP/SLAP)
13
+ *
14
+ * Usage:
15
+ * node examples/agent-demo.js [indelible-url]
16
+ *
17
+ * Default URL: http://localhost:4000
18
+ */
19
+
20
+ import { PrivateKey } from '@bsv/sdk'
21
+ import { IndelibleMemoryBridge } from '../src/bridge.js'
22
+ import { createAgentCertificate, verifyAgentCertificate } from '../src/identity.js'
23
+ import { signAction, verifyAction } from '../src/signing.js'
24
+ import { encryptMessage, decryptMessage } from '../src/encryption.js'
25
+ import { createServiceBroadcaster, createServiceResolver } from '../src/discovery.js'
26
+ import { PRICES } from '../src/constants.js'
27
+
28
+ const INDELIBLE_URL = process.argv[2] || 'http://localhost:4000'
29
+
30
+ // ────────────────────────────────────────────────────────────────
31
+ // Step 0: Generate keys
32
+ // ────────────────────────────────────────────────────────────────
33
+ console.log('═══════════════════════════════════════════════════')
34
+ console.log(' ClawSats + Indelible Agent Demo')
35
+ console.log(' All 6 BRC standards in action')
36
+ console.log('═══════════════════════════════════════════════════\n')
37
+
38
+ const operatorKey = PrivateKey.fromRandom()
39
+ const agentKey = PrivateKey.fromRandom()
40
+ const agent2Key = PrivateKey.fromRandom() // second agent for encryption demo
41
+
42
+ console.log('Keys generated:')
43
+ console.log(` Operator: ${operatorKey.toAddress()}`)
44
+ console.log(` Agent 1: ${agentKey.toAddress()}`)
45
+ console.log(` Agent 2: ${agent2Key.toAddress()}`)
46
+ console.log()
47
+
48
+ // ────────────────────────────────────────────────────────────────
49
+ // Step 1: BRC-52 — Agent Identity Certificate
50
+ // ────────────────────────────────────────────────────────────────
51
+ console.log('─── BRC-52: Agent Identity ───')
52
+
53
+ const cert = await createAgentCertificate({
54
+ operatorWif: operatorKey.toWif(),
55
+ agentPubKey: agentKey.toPublicKey().toString(),
56
+ agentName: 'DemoAgent',
57
+ capabilities: ['save_context', 'load_context']
58
+ })
59
+
60
+ console.log(` Certificate created (${cert.serialized.length} hex chars)`)
61
+
62
+ const verified = await verifyAgentCertificate(cert.serialized)
63
+ console.log(` Verified: ${verified.valid}`)
64
+ console.log(` Agent: ${verified.fields.agentName}`)
65
+ console.log(` Capabilities: ${verified.fields.capabilities.join(', ')}`)
66
+ console.log()
67
+
68
+ // ────────────────────────────────────────────────────────────────
69
+ // Step 2: Register operator with Indelible server
70
+ // ────────────────────────────────────────────────────────────────
71
+ console.log('─── Operator Registration ───')
72
+
73
+ try {
74
+ const regRes = await fetch(`${INDELIBLE_URL}/api/agents/register`, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify({
78
+ operatorAddress: operatorKey.toAddress(),
79
+ operatorWif: operatorKey.toWif()
80
+ })
81
+ })
82
+ const regData = await regRes.json()
83
+ console.log(` ${regData.message}`)
84
+ console.log(` Address: ${regData.address}`)
85
+ } catch (err) {
86
+ console.log(` Registration failed: ${err.message}`)
87
+ console.log(` (Is the server running at ${INDELIBLE_URL}?)`)
88
+ }
89
+ console.log()
90
+
91
+ // ────────────────────────────────────────────────────────────────
92
+ // Step 3: BRC-77 — Sign a save action
93
+ // ────────────────────────────────────────────────────────────────
94
+ console.log('─── BRC-77: Signed Actions ───')
95
+
96
+ const savePayload = {
97
+ agentAddress: agentKey.toAddress(),
98
+ summary: 'Demo agent conversation',
99
+ messageCount: 3
100
+ }
101
+
102
+ const signed = signAction({
103
+ privateKeyWif: agentKey.toWif(),
104
+ action: 'save_context',
105
+ payload: savePayload
106
+ })
107
+
108
+ console.log(` Action signed: ${signed.action}`)
109
+ console.log(` Timestamp: ${signed.timestamp}`)
110
+ console.log(` Signature: ${signed.signature.slice(0, 40)}...`)
111
+
112
+ const actionValid = verifyAction({
113
+ signature: signed.signature,
114
+ action: signed.action,
115
+ timestamp: signed.timestamp,
116
+ payload: savePayload
117
+ })
118
+
119
+ console.log(` Verified: ${actionValid}`)
120
+
121
+ // Tamper test
122
+ const tamperValid = verifyAction({
123
+ signature: signed.signature,
124
+ action: signed.action,
125
+ timestamp: signed.timestamp,
126
+ payload: { ...savePayload, messageCount: 999 }
127
+ })
128
+ console.log(` Tamper detected: ${!tamperValid}`)
129
+ console.log()
130
+
131
+ // ────────────────────────────────────────────────────────────────
132
+ // Step 4: Save agent memory via IndelibleMemoryBridge
133
+ // ────────────────────────────────────────────────────────────────
134
+ console.log('─── Memory Bridge: Save ───')
135
+
136
+ const bridge = new IndelibleMemoryBridge({
137
+ indelibleUrl: INDELIBLE_URL,
138
+ operatorAddress: operatorKey.toAddress(),
139
+ agentAddress: agentKey.toAddress()
140
+ })
141
+
142
+ try {
143
+ const messages = [
144
+ { role: 'user', content: 'What is the capital of France?' },
145
+ { role: 'assistant', content: 'The capital of France is Paris.' },
146
+ { role: 'user', content: 'What about Germany?' }
147
+ ]
148
+
149
+ const saveResult = await bridge.save('demo-agent', messages, {
150
+ summary: 'Geography Q&A session'
151
+ })
152
+
153
+ console.log(` Save type: ${saveResult.saveType}`)
154
+ console.log(` Session ID: ${saveResult.sessionId}`)
155
+ console.log(` TX ID: ${saveResult.txId}`)
156
+ console.log(` Messages: ${saveResult.messageCount}`)
157
+ console.log(` Cost: ${PRICES.save_context} sats`)
158
+ } catch (err) {
159
+ console.log(` Save failed: ${err.message}`)
160
+ console.log(` (This is expected if the operator wallet has no funds)`)
161
+ }
162
+ console.log()
163
+
164
+ // ────────────────────────────────────────────────────────────────
165
+ // Step 5: Load agent memory
166
+ // ────────────────────────────────────────────────────────────────
167
+ console.log('─── Memory Bridge: Load ───')
168
+
169
+ try {
170
+ const context = await bridge.load('demo-agent', { numSessions: 3 })
171
+ if (context) {
172
+ console.log(` Context restored (${context.length} chars)`)
173
+ console.log(` Preview: ${context.slice(0, 100)}...`)
174
+ } else {
175
+ console.log(' No context found (first run)')
176
+ }
177
+ console.log(` Cost: ${PRICES.load_context} sats`)
178
+ } catch (err) {
179
+ console.log(` Load failed: ${err.message}`)
180
+ }
181
+ console.log()
182
+
183
+ // ────────────────────────────────────────────────────────────────
184
+ // Step 6: BRC-78 — Encrypted agent-to-agent messaging
185
+ // ────────────────────────────────────────────────────────────────
186
+ console.log('─── BRC-78: Encrypted Messaging ───')
187
+
188
+ const secretMessage = {
189
+ from: 'DemoAgent',
190
+ to: 'Agent2',
191
+ content: 'Here is my session context for handoff',
192
+ sessionId: 'abc123'
193
+ }
194
+
195
+ const encrypted = encryptMessage({
196
+ senderWif: agentKey.toWif(),
197
+ recipientPubKey: agent2Key.toPublicKey().toString(),
198
+ message: secretMessage
199
+ })
200
+
201
+ console.log(` Encrypted: ${encrypted.slice(0, 40)}... (${encrypted.length} hex chars)`)
202
+
203
+ const decrypted = decryptMessage({
204
+ recipientWif: agent2Key.toWif(),
205
+ encryptedHex: encrypted,
206
+ parseJson: true
207
+ })
208
+
209
+ console.log(` Decrypted: ${decrypted.content}`)
210
+ console.log(` From: ${decrypted.from} → To: ${decrypted.to}`)
211
+
212
+ // Prove wrong key can't decrypt
213
+ try {
214
+ const wrongKey = PrivateKey.fromRandom()
215
+ decryptMessage({
216
+ recipientWif: wrongKey.toWif(),
217
+ encryptedHex: encrypted
218
+ })
219
+ console.log(' ERROR: Wrong key decrypted! (should not happen)')
220
+ } catch {
221
+ console.log(' Wrong key rejected (correct)')
222
+ }
223
+ console.log()
224
+
225
+ // ────────────────────────────────────────────────────────────────
226
+ // Step 7: SHIP/SLAP — Service Discovery
227
+ // ────────────────────────────────────────────────────────────────
228
+ console.log('─── SHIP/SLAP: Service Discovery ───')
229
+
230
+ const { broadcaster, topics } = createServiceBroadcaster({
231
+ capabilities: ['save_context', 'load_context']
232
+ })
233
+
234
+ console.log(` Broadcaster ready for topics:`)
235
+ for (const t of topics) {
236
+ console.log(` → ${t}`)
237
+ }
238
+
239
+ const { resolver, lookup } = createServiceResolver()
240
+ console.log(` Resolver ready`)
241
+ console.log(` (Actual SHIP/SLAP broadcast requires a funded wallet and overlay network)`)
242
+ console.log()
243
+
244
+ // ────────────────────────────────────────────────────────────────
245
+ // Summary
246
+ // ────────────────────────────────────────────────────────────────
247
+ console.log('═══════════════════════════════════════════════════')
248
+ console.log(' All 6 BRC standards demonstrated:')
249
+ console.log()
250
+ console.log(' ✓ BRC-52 Agent identity certificate — created & verified')
251
+ console.log(' ✓ BRC-105 Payment verification — middleware ready')
252
+ console.log(' ✓ BRC-31 Mutual authentication — AuthFetch client ready')
253
+ console.log(' ✓ BRC-77 Signed actions — save_context signed & verified')
254
+ console.log(' ✓ BRC-78 Encrypted messaging — agent-to-agent encryption works')
255
+ console.log(' ✓ SHIP/SLAP Service discovery — broadcaster & resolver ready')
256
+ console.log()
257
+ console.log(' Pricing:')
258
+ console.log(` save_context: ${PRICES.save_context} sats`)
259
+ console.log(` load_context: ${PRICES.load_context} sats`)
260
+ console.log(` protocol_fee: ${PRICES.protocol_fee} sats`)
261
+ console.log('═══════════════════════════════════════════════════')
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "clawsats-indelible",
3
+ "version": "0.2.0",
4
+ "description": "Persistent blockchain memory for ClawSats AI agents — powered by Indelible. BRC-105 payments, BRC-52 identity, BRC-31 auth, BRC-77 signing, BRC-78 encryption, SHIP/SLAP discovery.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./capabilities": "./src/capabilities.js",
10
+ "./bridge": "./src/bridge.js",
11
+ "./middleware": "./src/middleware.js",
12
+ "./identity": "./src/identity.js",
13
+ "./auth": "./src/auth.js",
14
+ "./signing": "./src/signing.js",
15
+ "./encryption": "./src/encryption.js",
16
+ "./discovery": "./src/discovery.js"
17
+ },
18
+ "dependencies": {
19
+ "@bsv/sdk": "^1.10.1",
20
+ "node-fetch": "^3.3.2"
21
+ },
22
+ "keywords": [
23
+ "clawsats",
24
+ "indelible",
25
+ "bsv",
26
+ "ai-agent",
27
+ "memory",
28
+ "blockchain",
29
+ "micropayments"
30
+ ],
31
+ "files": [
32
+ "src/",
33
+ "examples/"
34
+ ],
35
+ "author": "zcoolz",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/indelible-one/clawsats-indelible"
40
+ }
41
+ }
package/src/auth.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Mutual Authentication (BRC-31 Authrite)
3
+ *
4
+ * Replaces plain X-Operator-Address headers with proper cryptographic
5
+ * mutual authentication. Both sides prove identity on every request.
6
+ * No tokens to steal, no sessions to hijack.
7
+ *
8
+ * Uses AuthFetch from @bsv/sdk which implements the full Authrite protocol.
9
+ */
10
+
11
+ import { AuthFetch, ProtoWallet, PrivateKey } from '@bsv/sdk'
12
+
13
+ /**
14
+ * Create an authenticated HTTP client for agent-to-server communication
15
+ *
16
+ * @param {object} config
17
+ * @param {string} config.privateKeyWif - Agent or operator's private key (WIF)
18
+ * @returns {object} { fetch: function, wallet: ProtoWallet, publicKey: string }
19
+ */
20
+ export function createAuthClient(config) {
21
+ const { privateKeyWif } = config
22
+
23
+ if (!privateKeyWif) throw new Error('privateKeyWif required')
24
+
25
+ const key = PrivateKey.fromWif(privateKeyWif)
26
+ const wallet = new ProtoWallet(key)
27
+ const authFetch = new AuthFetch(wallet)
28
+
29
+ return {
30
+ /**
31
+ * Make an authenticated HTTP request (BRC-31 Authrite)
32
+ * Automatically handles mutual authentication and 402 payment flows
33
+ *
34
+ * @param {string} url - Full URL to request
35
+ * @param {object} options - { method, headers, body }
36
+ * @returns {Promise<Response>}
37
+ */
38
+ fetch: (url, options = {}) => authFetch.fetch(url, options),
39
+
40
+ /** The underlying ProtoWallet for signing/verifying */
41
+ wallet,
42
+
43
+ /** This client's public key (hex, compressed) */
44
+ publicKey: key.toPublicKey().toString()
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Verify an Authrite-authenticated request on the server side
50
+ *
51
+ * The server creates its own AuthFetch wallet to verify incoming
52
+ * requests are from legitimate agents with valid keys.
53
+ *
54
+ * @param {object} config
55
+ * @param {string} config.serverWif - Server's private key (WIF)
56
+ * @returns {object} { wallet: ProtoWallet, publicKey: string }
57
+ */
58
+ export function createAuthServer(config) {
59
+ const { serverWif } = config
60
+
61
+ if (!serverWif) throw new Error('serverWif required')
62
+
63
+ const key = PrivateKey.fromWif(serverWif)
64
+ const wallet = new ProtoWallet(key)
65
+
66
+ return {
67
+ wallet,
68
+ publicKey: key.toPublicKey().toString()
69
+ }
70
+ }
package/src/bridge.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * IndelibleMemoryBridge
3
+ * Drop-in replacement for ClawSats' OnChainMemory (CLAWMEM_V1)
4
+ *
5
+ * Advantages over OnChainMemory:
6
+ * - Chunked transactions (unlimited payload via delta saves)
7
+ * - SPV bridge (no WhatsOnChain dependency)
8
+ * - Structured JSONL with session chaining
9
+ * - Delta saves (only new messages committed)
10
+ * - AES-256-GCM encryption per agent
11
+ * - Smart restore (tail-heavy priority)
12
+ * - Redis-indexed (recoverable from chain if index lost)
13
+ */
14
+
15
+ import fetch from 'node-fetch'
16
+ import { DEFAULT_INDELIBLE_URL } from './constants.js'
17
+
18
+ export class IndelibleMemoryBridge {
19
+ /**
20
+ * @param {object} config
21
+ * @param {string} config.indelibleUrl - Indelible server URL
22
+ * @param {string} config.operatorAddress - BSV address of the operator
23
+ * @param {string} config.agentAddress - BSV address of this agent
24
+ */
25
+ constructor(config) {
26
+ this.indelibleUrl = config.indelibleUrl || DEFAULT_INDELIBLE_URL
27
+ this.operatorAddress = config.operatorAddress
28
+ this.agentAddress = config.agentAddress
29
+
30
+ if (!this.operatorAddress) throw new Error('operatorAddress required')
31
+ if (!this.agentAddress) throw new Error('agentAddress required')
32
+ }
33
+
34
+ /**
35
+ * Save agent memory to blockchain
36
+ * Drop-in replacement for OnChainMemory.save(key, data)
37
+ *
38
+ * @param {string} key - Agent identifier / memory key
39
+ * @param {Array|object} data - Messages array or raw data object
40
+ * @param {object} options
41
+ * @param {string} options.summary - Human-readable summary
42
+ * @returns {object} { success, txId, sessionId, messageCount, saveType }
43
+ */
44
+ async save(key, data, options = {}) {
45
+ const messages = Array.isArray(data)
46
+ ? data
47
+ : [{ role: 'system', content: JSON.stringify(data) }]
48
+
49
+ const result = await this._call('/api/agents/save', {
50
+ agentAddress: this.agentAddress,
51
+ agentId: key,
52
+ messages,
53
+ summary: options.summary || `Agent memory: ${key}`,
54
+ operatorAddress: this.operatorAddress
55
+ })
56
+
57
+ return result
58
+ }
59
+
60
+ /**
61
+ * Load agent memory from blockchain
62
+ * Drop-in replacement for OnChainMemory.load(key)
63
+ *
64
+ * @param {string} key - Agent identifier (unused in v1, loads by address)
65
+ * @param {object} options
66
+ * @param {number} options.numSessions - Number of sessions to restore (default 3)
67
+ * @returns {string|null} Formatted context string or null
68
+ */
69
+ async load(key, options = {}) {
70
+ const result = await this._call('/api/agents/load', {
71
+ agentAddress: this.agentAddress,
72
+ numSessions: options.numSessions || 3,
73
+ operatorAddress: this.operatorAddress
74
+ })
75
+
76
+ return result.context || null
77
+ }
78
+
79
+ /**
80
+ * List all sessions for this agent
81
+ * @returns {Array} Session metadata (no content, just summaries/timestamps/txIds)
82
+ */
83
+ async list() {
84
+ const res = await fetch(
85
+ `${this.indelibleUrl}/api/agents/sessions/${this.agentAddress}`,
86
+ {
87
+ headers: {
88
+ 'X-Operator-Address': this.operatorAddress
89
+ }
90
+ }
91
+ )
92
+
93
+ if (!res.ok) {
94
+ const err = await res.text()
95
+ throw new Error(`List failed (${res.status}): ${err}`)
96
+ }
97
+
98
+ const data = await res.json()
99
+ return data.sessions || []
100
+ }
101
+
102
+ /**
103
+ * Internal: POST to Indelible server
104
+ */
105
+ async _call(path, body) {
106
+ const res = await fetch(`${this.indelibleUrl}${path}`, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ 'X-Operator-Address': this.operatorAddress
111
+ },
112
+ body: JSON.stringify(body)
113
+ })
114
+
115
+ if (!res.ok) {
116
+ const err = await res.text()
117
+ throw new Error(`Indelible ${path} failed (${res.status}): ${err}`)
118
+ }
119
+
120
+ return res.json()
121
+ }
122
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ClawSats Capability Registration
3
+ * Registers save_context and load_context as paid ClawSats capabilities
4
+ */
5
+
6
+ import fetch from 'node-fetch'
7
+ import { PRICES, CAPABILITY_TAGS, DEFAULT_INDELIBLE_URL } from './constants.js'
8
+
9
+ /**
10
+ * Register Indelible memory capabilities with a ClawSats CapabilityRegistry
11
+ *
12
+ * @param {object} registry - ClawSats CapabilityRegistry instance
13
+ * @param {object} config
14
+ * @param {string} config.indelibleUrl - Indelible server URL (default: https://indelible.one)
15
+ * @param {string} config.operatorAddress - BSV address of the operator running this node
16
+ */
17
+ export function registerIndelibleCapabilities(registry, config) {
18
+ const {
19
+ indelibleUrl = DEFAULT_INDELIBLE_URL,
20
+ operatorAddress
21
+ } = config
22
+
23
+ if (!operatorAddress) throw new Error('operatorAddress required')
24
+
25
+ // save_context — 15 sats
26
+ // Agent sends messages + summary, gets txId back
27
+ registry.register({
28
+ name: 'save_context',
29
+ description: 'Save agent memory to BSV blockchain via Indelible SPV bridge. Supports delta saves, session chaining, and AES-256-GCM encrypted storage. Your data survives crashes, migrations, and host death.',
30
+ pricePerCall: PRICES.save_context,
31
+ tags: CAPABILITY_TAGS.save,
32
+ handler: async (params) => {
33
+ const { messages, summary, agentAddress, agentId } = params
34
+
35
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
36
+ throw new Error('messages array required')
37
+ }
38
+ if (!agentAddress) {
39
+ throw new Error('agentAddress required')
40
+ }
41
+
42
+ const response = await fetch(`${indelibleUrl}/api/agents/save`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'X-Operator-Address': operatorAddress
47
+ },
48
+ body: JSON.stringify({
49
+ messages,
50
+ summary: summary || `Agent ${agentId || agentAddress} session`,
51
+ agentAddress,
52
+ agentId: agentId || agentAddress,
53
+ operatorAddress
54
+ })
55
+ })
56
+
57
+ if (!response.ok) {
58
+ const err = await response.text()
59
+ throw new Error(`Save failed (${response.status}): ${err}`)
60
+ }
61
+
62
+ return response.json()
63
+ }
64
+ })
65
+
66
+ // load_context — 10 sats
67
+ // Agent sends its address, gets restored context back
68
+ registry.register({
69
+ name: 'load_context',
70
+ description: 'Load agent memory from BSV blockchain via Indelible. Smart restore with tail-heavy priority — recent messages in full, older ones summarized. Merges delta saves automatically.',
71
+ pricePerCall: PRICES.load_context,
72
+ tags: CAPABILITY_TAGS.load,
73
+ handler: async (params) => {
74
+ const { agentAddress, numSessions = 3 } = params
75
+
76
+ if (!agentAddress) {
77
+ throw new Error('agentAddress required')
78
+ }
79
+
80
+ const response = await fetch(`${indelibleUrl}/api/agents/load`, {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ 'X-Operator-Address': operatorAddress
85
+ },
86
+ body: JSON.stringify({
87
+ agentAddress,
88
+ numSessions,
89
+ operatorAddress
90
+ })
91
+ })
92
+
93
+ if (!response.ok) {
94
+ const err = await response.text()
95
+ throw new Error(`Load failed (${response.status}): ${err}`)
96
+ }
97
+
98
+ return response.json()
99
+ }
100
+ })
101
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ClawSats-Indelible Constants
3
+ * Pricing, protocol tags, and configuration defaults
4
+ */
5
+
6
+ export const PRICES = {
7
+ save_context: 15, // sats paid to operator per save
8
+ load_context: 10, // sats paid to operator per load
9
+ protocol_fee: 2 // sats ClawSats protocol fee per call
10
+ }
11
+
12
+ export const PROTOCOL_TAG = 'indelible.agent'
13
+ export const DEFAULT_INDELIBLE_URL = 'https://indelible.one'
14
+
15
+ export const CAPABILITY_TAGS = {
16
+ save: ['memory', 'persistence', 'blockchain', 'indelible'],
17
+ load: ['memory', 'recall', 'blockchain', 'indelible']
18
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Service Discovery (SHIP/SLAP)
3
+ *
4
+ * Agents advertise what they can do (SHIP) and discover other agents'
5
+ * capabilities (SLAP). An agent broadcasts "I offer save_context" and
6
+ * others can find it through the overlay network.
7
+ *
8
+ * SHIP = Service Host Interconnect Protocol (advertise services)
9
+ * SLAP = Service Lookup Availability Protocol (discover services)
10
+ *
11
+ * Uses SHIPBroadcaster and LookupResolver from @bsv/sdk.
12
+ */
13
+
14
+ import { SHIPBroadcaster, LookupResolver } from '@bsv/sdk'
15
+
16
+ /** Default topic prefix for Indelible agent services */
17
+ const TOPIC_PREFIX = 'tm_indelible_'
18
+
19
+ /**
20
+ * Create a service broadcaster for advertising agent capabilities
21
+ *
22
+ * Agents use this to announce what services they offer on the overlay network.
23
+ * Other agents can then discover them via SLAP lookup.
24
+ *
25
+ * @param {object} config
26
+ * @param {string[]} config.capabilities - Services this agent offers (e.g. ['save_context', 'load_context'])
27
+ * @param {string} config.network - 'mainnet' | 'testnet' (default: 'mainnet')
28
+ * @returns {object} { broadcaster: SHIPBroadcaster, topics: string[] }
29
+ */
30
+ export function createServiceBroadcaster(config) {
31
+ const { capabilities, network = 'mainnet' } = config
32
+
33
+ if (!capabilities || !capabilities.length) throw new Error('capabilities array required')
34
+
35
+ const topics = capabilities.map(cap => `${TOPIC_PREFIX}${cap}`)
36
+
37
+ const broadcaster = new SHIPBroadcaster(topics, {
38
+ networkPreset: network
39
+ })
40
+
41
+ return { broadcaster, topics }
42
+ }
43
+
44
+ /**
45
+ * Create a service resolver for discovering agent capabilities
46
+ *
47
+ * Operators use this to find agents that offer specific services.
48
+ *
49
+ * @param {object} config
50
+ * @param {string} config.network - 'mainnet' | 'testnet' (default: 'mainnet')
51
+ * @returns {object} { resolver: LookupResolver, lookup: function }
52
+ */
53
+ export function createServiceResolver(config = {}) {
54
+ const { network = 'mainnet' } = config
55
+
56
+ const resolver = new LookupResolver({
57
+ networkPreset: network
58
+ })
59
+
60
+ return {
61
+ resolver,
62
+
63
+ /**
64
+ * Look up agents offering a specific capability
65
+ *
66
+ * @param {string} capability - Service to search for (e.g. 'save_context')
67
+ * @param {number} timeout - Timeout in ms (default: 5000)
68
+ * @returns {Promise<object>} Lookup answer with available service providers
69
+ */
70
+ lookup: (capability, timeout = 5000) => {
71
+ const topic = `${TOPIC_PREFIX}${capability}`
72
+ return resolver.query({
73
+ service: 'ls_slap',
74
+ query: { topic }
75
+ }, timeout)
76
+ }
77
+ }
78
+ }
79
+
80
+ export { TOPIC_PREFIX }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Encrypted Messages (BRC-78)
3
+ *
4
+ * Agent-to-agent private communication. Two agents can exchange
5
+ * encrypted data that only the intended recipient can decrypt.
6
+ * Different from per-agent AES storage encryption — this is for
7
+ * direct agent-to-agent messaging.
8
+ *
9
+ * Uses EncryptedMessage from @bsv/sdk which implements BRC-78.
10
+ */
11
+
12
+ import { EncryptedMessage, PrivateKey, PublicKey, Utils } from '@bsv/sdk'
13
+
14
+ /**
15
+ * Encrypt a message from one agent to another
16
+ *
17
+ * @param {object} config
18
+ * @param {string} config.senderWif - Sender's private key (WIF)
19
+ * @param {string} config.recipientPubKey - Recipient's public key (hex, compressed)
20
+ * @param {string|object} config.message - Message to encrypt (string or object → JSON)
21
+ * @returns {string} Encrypted message as hex
22
+ */
23
+ export function encryptMessage(config) {
24
+ const { senderWif, recipientPubKey, message } = config
25
+
26
+ if (!senderWif) throw new Error('senderWif required')
27
+ if (!recipientPubKey) throw new Error('recipientPubKey required')
28
+ if (message === undefined || message === null) throw new Error('message required')
29
+
30
+ const senderKey = PrivateKey.fromWif(senderWif)
31
+ const recipientKey = PublicKey.fromString(recipientPubKey)
32
+
33
+ const text = typeof message === 'string' ? message : JSON.stringify(message)
34
+ const messageBytes = Utils.toArray(text, 'utf8')
35
+
36
+ const encrypted = EncryptedMessage.encrypt(messageBytes, senderKey, recipientKey)
37
+ return Utils.toHex(encrypted)
38
+ }
39
+
40
+ /**
41
+ * Decrypt a message received from another agent
42
+ *
43
+ * @param {object} config
44
+ * @param {string} config.recipientWif - Recipient's private key (WIF)
45
+ * @param {string} config.encryptedHex - Encrypted message hex from encryptMessage
46
+ * @param {boolean} config.parseJson - If true, parse the decrypted text as JSON (default: false)
47
+ * @returns {string|object} Decrypted message
48
+ */
49
+ export function decryptMessage(config) {
50
+ const { recipientWif, encryptedHex, parseJson = false } = config
51
+
52
+ if (!recipientWif) throw new Error('recipientWif required')
53
+ if (!encryptedHex) throw new Error('encryptedHex required')
54
+
55
+ const recipientKey = PrivateKey.fromWif(recipientWif)
56
+ const encryptedBytes = Utils.toArray(encryptedHex, 'hex')
57
+
58
+ const decrypted = EncryptedMessage.decrypt(encryptedBytes, recipientKey)
59
+ const text = Utils.toUTF8(decrypted)
60
+
61
+ return parseJson ? JSON.parse(text) : text
62
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Agent Identity (BRC-52 Certificates)
3
+ *
4
+ * Agents register with verifiable identity certificates signed by their operator.
5
+ * Certificates prove: who the agent is, who operates it, and what it can do.
6
+ * Other parties verify the certificate without trusting the server.
7
+ */
8
+
9
+ import { Certificate, ProtoWallet, PrivateKey, Hash, Utils, Random } from '@bsv/sdk'
10
+
11
+ // Fixed certificate type for Indelible agent identity
12
+ // SHA-256("indelible.agent.identity.v1") → exactly 32 bytes as base64
13
+ const AGENT_CERT_TYPE = Utils.toBase64(
14
+ Hash.sha256(Utils.toArray('indelible.agent.identity.v1', 'utf8'))
15
+ )
16
+
17
+ /**
18
+ * Create a signed agent identity certificate
19
+ *
20
+ * @param {object} config
21
+ * @param {string} config.operatorWif - Operator's private key (WIF) — signs the certificate
22
+ * @param {string} config.agentPubKey - Agent's public key (hex, compressed)
23
+ * @param {string} config.agentName - Human-readable agent name
24
+ * @param {string[]} config.capabilities - What this agent can do (e.g. ['save_context', 'load_context'])
25
+ * @returns {object} { certificate: Certificate, serialized: string (hex) }
26
+ */
27
+ export async function createAgentCertificate(config) {
28
+ const { operatorWif, agentPubKey, agentName, capabilities = [] } = config
29
+
30
+ if (!operatorWif) throw new Error('operatorWif required')
31
+ if (!agentPubKey) throw new Error('agentPubKey required')
32
+ if (!agentName) throw new Error('agentName required')
33
+
34
+ const operatorKey = PrivateKey.fromWif(operatorWif)
35
+ const serialNumber = Utils.toBase64(Random(32))
36
+
37
+ const fields = {
38
+ agentName: Utils.toBase64(Utils.toArray(agentName, 'utf8')),
39
+ capabilities: Utils.toBase64(Utils.toArray(JSON.stringify(capabilities), 'utf8')),
40
+ registeredAt: Utils.toBase64(Utils.toArray(new Date().toISOString(), 'utf8'))
41
+ }
42
+
43
+ const cert = new Certificate(
44
+ AGENT_CERT_TYPE,
45
+ serialNumber,
46
+ agentPubKey,
47
+ operatorKey.toPublicKey().toString(),
48
+ '0000000000000000000000000000000000000000000000000000000000000000.0',
49
+ fields
50
+ )
51
+
52
+ const wallet = new ProtoWallet(operatorKey)
53
+ await cert.sign(wallet)
54
+
55
+ return {
56
+ certificate: cert,
57
+ serialized: Utils.toHex(cert.toBinary())
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Verify an agent identity certificate
63
+ *
64
+ * @param {string} serializedHex - Certificate in hex (from createAgentCertificate)
65
+ * @returns {object} { valid: boolean, subject, certifier, fields: { agentName, capabilities, registeredAt } }
66
+ */
67
+ export async function verifyAgentCertificate(serializedHex) {
68
+ const bin = Utils.toArray(serializedHex, 'hex')
69
+ const cert = Certificate.fromBinary(bin)
70
+
71
+ const valid = await cert.verify()
72
+
73
+ // Decode fields from base64
74
+ const decode = (b64) => {
75
+ try {
76
+ return Utils.toUTF8(Utils.toArray(b64, 'base64'))
77
+ } catch {
78
+ return b64
79
+ }
80
+ }
81
+
82
+ const fields = {}
83
+ if (cert.fields.agentName) fields.agentName = decode(cert.fields.agentName)
84
+ if (cert.fields.capabilities) {
85
+ try {
86
+ fields.capabilities = JSON.parse(decode(cert.fields.capabilities))
87
+ } catch {
88
+ fields.capabilities = []
89
+ }
90
+ }
91
+ if (cert.fields.registeredAt) fields.registeredAt = decode(cert.fields.registeredAt)
92
+
93
+ return {
94
+ valid,
95
+ subject: cert.subject,
96
+ certifier: cert.certifier,
97
+ serialNumber: cert.serialNumber,
98
+ fields
99
+ }
100
+ }
101
+
102
+ export { AGENT_CERT_TYPE }
package/src/index.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * clawsats-indelible
3
+ * Persistent blockchain memory for ClawSats AI agents
4
+ *
5
+ * Gives ClawSats agents what they're missing: permanent, portable,
6
+ * self-sovereign memory that survives crashes, migrations, and host death.
7
+ *
8
+ * BRC Standards implemented:
9
+ * BRC-105 — Payment verification (middleware.js)
10
+ * BRC-52 — Agent identity certificates (identity.js)
11
+ * BRC-31 — Mutual authentication / Authrite (auth.js)
12
+ * BRC-77 — Signed messages (signing.js)
13
+ * BRC-78 — Encrypted messages (encryption.js)
14
+ * SHIP/SLAP — Service discovery (discovery.js)
15
+ */
16
+
17
+ // Core
18
+ export { registerIndelibleCapabilities } from './capabilities.js'
19
+ export { IndelibleMemoryBridge } from './bridge.js'
20
+ export { createIndeliblePaymentMiddleware } from './middleware.js'
21
+ export { PRICES, PROTOCOL_TAG, DEFAULT_INDELIBLE_URL, CAPABILITY_TAGS } from './constants.js'
22
+
23
+ // BRC-52 — Agent Identity
24
+ export { createAgentCertificate, verifyAgentCertificate, AGENT_CERT_TYPE } from './identity.js'
25
+
26
+ // BRC-31 — Mutual Authentication
27
+ export { createAuthClient, createAuthServer } from './auth.js'
28
+
29
+ // BRC-77 — Signed Messages
30
+ export { signAction, verifyAction } from './signing.js'
31
+
32
+ // BRC-78 — Encrypted Messages
33
+ export { encryptMessage, decryptMessage } from './encryption.js'
34
+
35
+ // SHIP/SLAP — Service Discovery
36
+ export { createServiceBroadcaster, createServiceResolver, TOPIC_PREFIX } from './discovery.js'
@@ -0,0 +1,122 @@
1
+ /**
2
+ * BRC-105 Payment Middleware (v2 — Real Verification)
3
+ *
4
+ * Implements the HTTP 402 Payment Required flow for Indelible capabilities.
5
+ * When a request comes in without payment, returns 402 with required headers.
6
+ * When payment is included, parses the raw transaction and verifies an output
7
+ * pays the operator address the required amount.
8
+ */
9
+
10
+ import crypto from 'crypto'
11
+ import { Transaction, P2PKH } from '@bsv/sdk'
12
+
13
+ /**
14
+ * Create Express middleware for BRC-105 payment gating
15
+ *
16
+ * @param {object} config
17
+ * @param {string} config.operatorAddress - BSV address to receive payments
18
+ * @param {function} config.calculatePrice - (req) => sats required
19
+ * @returns {function} Express middleware
20
+ */
21
+ export function createIndeliblePaymentMiddleware(config) {
22
+ const { operatorAddress, calculatePrice } = config
23
+ const usedPrefixes = new Set()
24
+
25
+ if (!operatorAddress) throw new Error('operatorAddress required')
26
+ if (!calculatePrice) throw new Error('calculatePrice function required')
27
+
28
+ return async (req, res, next) => {
29
+ const price = await calculatePrice(req)
30
+
31
+ // Free calls pass through
32
+ if (price === 0) {
33
+ req.payment = { satoshisPaid: 0, accepted: true }
34
+ return next()
35
+ }
36
+
37
+ const paymentHeader = req.headers['x-bsv-payment']
38
+
39
+ // No payment: return 402 challenge
40
+ if (!paymentHeader) {
41
+ const prefix = crypto.randomBytes(16).toString('base64')
42
+
43
+ res.set('x-bsv-payment-version', '1.0')
44
+ res.set('x-bsv-payment-satoshis-required', String(price))
45
+ res.set('x-bsv-payment-derivation-prefix', prefix)
46
+ res.set('x-bsv-payment-address', operatorAddress)
47
+
48
+ return res.status(402).json({
49
+ status: 'error',
50
+ code: 'ERR_PAYMENT_REQUIRED',
51
+ satoshisRequired: price,
52
+ operatorAddress,
53
+ description: `Pay ${price} sats to use Indelible memory service`
54
+ })
55
+ }
56
+
57
+ // Payment included: verify
58
+ try {
59
+ const payment = JSON.parse(paymentHeader)
60
+ const { derivationPrefix, transaction } = payment
61
+
62
+ // Replay protection
63
+ if (usedPrefixes.has(derivationPrefix)) {
64
+ return res.status(400).json({ error: 'Payment prefix already used (replay detected)' })
65
+ }
66
+
67
+ // BRC-105 v2: Parse raw transaction and verify payment output
68
+ if (!transaction || typeof transaction !== 'string') {
69
+ return res.status(400).json({ error: 'Invalid payment transaction' })
70
+ }
71
+
72
+ // Parse the transaction hex
73
+ let tx
74
+ try {
75
+ tx = Transaction.fromHex(transaction)
76
+ } catch (parseErr) {
77
+ return res.status(400).json({ error: `Invalid transaction hex: ${parseErr.message}` })
78
+ }
79
+
80
+ // Build expected locking script for operator address
81
+ const p2pkh = new P2PKH()
82
+ const expectedScript = p2pkh.lock(operatorAddress).toHex()
83
+
84
+ // Find output(s) paying to operator address
85
+ let satoshisPaid = 0
86
+ for (const output of tx.outputs) {
87
+ if (output.lockingScript.toHex() === expectedScript) {
88
+ satoshisPaid += output.satoshis
89
+ }
90
+ }
91
+
92
+ if (satoshisPaid < price) {
93
+ return res.status(400).json({
94
+ error: `Insufficient payment: ${satoshisPaid} sats paid, ${price} required`,
95
+ satoshisPaid,
96
+ satoshisRequired: price
97
+ })
98
+ }
99
+
100
+ usedPrefixes.add(derivationPrefix)
101
+
102
+ // Clean up old prefixes (memory management)
103
+ if (usedPrefixes.size > 10000) {
104
+ const iterator = usedPrefixes.values()
105
+ for (let i = 0; i < 5000; i++) {
106
+ usedPrefixes.delete(iterator.next().value)
107
+ }
108
+ }
109
+
110
+ req.payment = {
111
+ satoshisPaid,
112
+ accepted: true,
113
+ derivationPrefix,
114
+ txid: tx.id('hex')
115
+ }
116
+
117
+ next()
118
+ } catch (err) {
119
+ res.status(400).json({ error: `Payment verification failed: ${err.message}` })
120
+ }
121
+ }
122
+ }
package/src/signing.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Signed Messages (BRC-77)
3
+ *
4
+ * Agents cryptographically sign their actions (saves, loads, requests).
5
+ * Anyone can verify "agent X really did this" — not spoofable.
6
+ *
7
+ * Uses SignedMessage from @bsv/sdk which implements BRC-77.
8
+ */
9
+
10
+ import { SignedMessage, PrivateKey, Utils } from '@bsv/sdk'
11
+
12
+ /**
13
+ * Sign an agent action
14
+ *
15
+ * @param {object} config
16
+ * @param {string} config.privateKeyWif - Agent's private key (WIF)
17
+ * @param {string} config.action - Action name (e.g. 'save_context', 'load_context')
18
+ * @param {object} config.payload - Action payload to sign
19
+ * @returns {object} { signature: string (hex), publicKey: string, action, timestamp }
20
+ */
21
+ export function signAction(config) {
22
+ const { privateKeyWif, action, payload } = config
23
+
24
+ if (!privateKeyWif) throw new Error('privateKeyWif required')
25
+ if (!action) throw new Error('action required')
26
+
27
+ const key = PrivateKey.fromWif(privateKeyWif)
28
+ const timestamp = new Date().toISOString()
29
+
30
+ // Build canonical message: action + timestamp + sorted payload JSON
31
+ const message = JSON.stringify({ action, timestamp, payload })
32
+ const messageBytes = Utils.toArray(message, 'utf8')
33
+
34
+ const sig = SignedMessage.sign(messageBytes, key)
35
+
36
+ return {
37
+ signature: Utils.toHex(sig),
38
+ publicKey: key.toPublicKey().toString(),
39
+ action,
40
+ timestamp
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Verify a signed agent action
46
+ *
47
+ * @param {object} config
48
+ * @param {string} config.signature - Signature hex from signAction
49
+ * @param {string} config.action - Action name
50
+ * @param {string} config.timestamp - ISO timestamp from signAction
51
+ * @param {object} config.payload - Original payload
52
+ * @returns {boolean} true if signature is valid
53
+ */
54
+ export function verifyAction(config) {
55
+ const { signature, action, timestamp, payload } = config
56
+
57
+ if (!signature) throw new Error('signature required')
58
+ if (!action) throw new Error('action required')
59
+
60
+ const message = JSON.stringify({ action, timestamp, payload })
61
+ const messageBytes = Utils.toArray(message, 'utf8')
62
+ const sigBytes = Utils.toArray(signature, 'hex')
63
+
64
+ return SignedMessage.verify(messageBytes, sigBytes)
65
+ }