@svsprotocol/solana 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 +158 -0
- package/README.md +365 -0
- package/dist/action-production-proof-evidence.js +553 -0
- package/dist/adapter-catalog.d.ts +29 -0
- package/dist/adapter-catalog.js +146 -0
- package/dist/adapter-core.d.ts +48 -0
- package/dist/adapter-core.js +249 -0
- package/dist/approval-signature.js +197 -0
- package/dist/base58.js +69 -0
- package/dist/bot-auth.js +50 -0
- package/dist/bot-certification-evidence.js +342 -0
- package/dist/bot-first-action-runbook.js +299 -0
- package/dist/bot-integration-contract.js +41 -0
- package/dist/certified-submit-status.js +176 -0
- package/dist/common.d.ts +1135 -0
- package/dist/elizaos.d.ts +43 -0
- package/dist/elizaos.js +227 -0
- package/dist/goat.d.ts +47 -0
- package/dist/goat.js +261 -0
- package/dist/index.d.ts +330 -0
- package/dist/index.js +128 -0
- package/dist/protocol.d.ts +205 -0
- package/dist/protocol.js +900 -0
- package/dist/receipt.js +51 -0
- package/dist/signed-proof-read-protection.js +495 -0
- package/dist/solana-agent-kit.d.ts +35 -0
- package/dist/solana-agent-kit.js +151 -0
- package/dist/svs-client.js +1232 -0
- package/dist/vercel-ai.d.ts +47 -0
- package/dist/vercel-ai.js +266 -0
- package/dist/verified-agent-adoption-kit.js +471 -0
- package/dist/verified-agent-profile.js +329 -0
- package/dist/verified-agent-registry-consumer.js +421 -0
- package/dist/verified-agent-registry.d.ts +36 -0
- package/dist/verified-agent-registry.js +826 -0
- package/dist/verified-agent-trust-score.js +335 -0
- package/dist/webhooks.js +834 -0
- package/package.json +72 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const SVS_ADAPTER_CATALOG_VERSION = "svs.agent-framework-adapter-catalog.v1";
|
|
2
|
+
|
|
3
|
+
const FRAMEWORK_ADAPTERS = [
|
|
4
|
+
{
|
|
5
|
+
id: "custom-adapter-core",
|
|
6
|
+
runtime: "Custom agent runtime",
|
|
7
|
+
category: "adapter-core",
|
|
8
|
+
packageExport: "@svsprotocol/solana/adapter-core",
|
|
9
|
+
exportPath: "./adapter-core",
|
|
10
|
+
adapterVersion: "svs.adapter-core.v1",
|
|
11
|
+
readinessHelper: "requireSvsAdapterProductionReady",
|
|
12
|
+
submitHelper: "verifyAndSubmitSvsAdapterSolanaAction",
|
|
13
|
+
toolName: null,
|
|
14
|
+
pluginName: null,
|
|
15
|
+
actionName: null,
|
|
16
|
+
demoCommand: "npm run example:typescript:agent",
|
|
17
|
+
docsPath: "examples/host-apps/custom-adapter-core/README.md",
|
|
18
|
+
hostValidationTarget: "custom-adapter-core",
|
|
19
|
+
bestFit: "Any Solana agent framework that wants SVS without a first-party adapter.",
|
|
20
|
+
primary: false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "solana-agent-kit",
|
|
24
|
+
runtime: "Solana Agent Kit",
|
|
25
|
+
category: "framework-adapter",
|
|
26
|
+
packageExport: "@svsprotocol/solana/solana-agent-kit",
|
|
27
|
+
exportPath: "./solana-agent-kit",
|
|
28
|
+
adapterVersion: "svs.solana-agent-kit-adapter.v1",
|
|
29
|
+
readinessHelper: "requireSvsProductionReady",
|
|
30
|
+
submitHelper: "verifyAndSubmitSolanaAction",
|
|
31
|
+
toolName: "svs_verify_and_submit_solana_action",
|
|
32
|
+
pluginName: null,
|
|
33
|
+
actionName: null,
|
|
34
|
+
demoCommand: "npm run example:agent:solana-kit",
|
|
35
|
+
docsPath: "integrations/solana-agent-kit/README.md",
|
|
36
|
+
hostValidationTarget: "solana-agent-kit-minimal",
|
|
37
|
+
bestFit: "Solana-native agent kits that register executable tools.",
|
|
38
|
+
primary: true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "elizaos",
|
|
42
|
+
runtime: "ElizaOS",
|
|
43
|
+
category: "framework-adapter",
|
|
44
|
+
packageExport: "@svsprotocol/solana/elizaos",
|
|
45
|
+
exportPath: "./elizaos",
|
|
46
|
+
adapterVersion: "svs.elizaos-adapter.v1",
|
|
47
|
+
readinessHelper: "requireSvsElizaOsProductionReady",
|
|
48
|
+
submitHelper: "verifyAndSubmitElizaOsSolanaAction",
|
|
49
|
+
toolName: null,
|
|
50
|
+
pluginName: "svs-solana-verification",
|
|
51
|
+
actionName: "SVS_VERIFY_AND_SUBMIT_SOLANA_ACTION",
|
|
52
|
+
demoCommand: "npm run example:agent:elizaos",
|
|
53
|
+
docsPath: "integrations/elizaos/README.md",
|
|
54
|
+
hostValidationTarget: "elizaos-plugin-install",
|
|
55
|
+
bestFit: "ElizaOS plugins/actions with fail-closed handler results.",
|
|
56
|
+
primary: true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "goat",
|
|
60
|
+
runtime: "GOAT",
|
|
61
|
+
category: "framework-adapter",
|
|
62
|
+
packageExport: "@svsprotocol/solana/goat",
|
|
63
|
+
exportPath: "./goat",
|
|
64
|
+
adapterVersion: "svs.goat-adapter.v1",
|
|
65
|
+
readinessHelper: "requireSvsGoatProductionReady",
|
|
66
|
+
submitHelper: "verifyAndSubmitGoatSolanaAction",
|
|
67
|
+
toolName: "svs_verify_and_submit_solana_action",
|
|
68
|
+
pluginName: "svs-solana-verification",
|
|
69
|
+
actionName: null,
|
|
70
|
+
demoCommand: "npm run example:agent:goat",
|
|
71
|
+
docsPath: "integrations/goat/README.md",
|
|
72
|
+
hostValidationTarget: "goat-sdk-plugin-install",
|
|
73
|
+
bestFit: "GOAT finance agents and Solana transaction tools.",
|
|
74
|
+
primary: true
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "vercel-ai-sdk",
|
|
78
|
+
runtime: "Vercel AI SDK",
|
|
79
|
+
category: "framework-adapter",
|
|
80
|
+
packageExport: "@svsprotocol/solana/vercel-ai",
|
|
81
|
+
exportPath: "./vercel-ai",
|
|
82
|
+
adapterVersion: "svs.vercel-ai-sdk-adapter.v1",
|
|
83
|
+
readinessHelper: "requireSvsVercelAiSdkProductionReady",
|
|
84
|
+
submitHelper: "verifyAndSubmitVercelAiSdkSolanaAction",
|
|
85
|
+
toolName: "svsVerifyAndSubmitSolanaAction",
|
|
86
|
+
pluginName: null,
|
|
87
|
+
actionName: null,
|
|
88
|
+
demoCommand: "npm run example:agent:vercel-ai",
|
|
89
|
+
docsPath: "integrations/vercel-ai/README.md",
|
|
90
|
+
hostValidationTarget: "vercel-ai-sdk-tool-install",
|
|
91
|
+
bestFit: "AI SDK generateText and streamText tool maps.",
|
|
92
|
+
primary: true
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
export const SVS_AGENT_FRAMEWORK_ADAPTERS = Object.freeze(
|
|
97
|
+
FRAMEWORK_ADAPTERS.map((adapter) => Object.freeze({ ...adapter }))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export function getSvsAgentFrameworkAdapters({
|
|
101
|
+
includeCustom = true,
|
|
102
|
+
primaryOnly = false
|
|
103
|
+
} = {}) {
|
|
104
|
+
return SVS_AGENT_FRAMEWORK_ADAPTERS
|
|
105
|
+
.filter((adapter) => includeCustom || adapter.id !== "custom-adapter-core")
|
|
106
|
+
.filter((adapter) => !primaryOnly || adapter.primary === true)
|
|
107
|
+
.map((adapter) => ({ ...adapter }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function findSvsAgentFrameworkAdapter(identifier) {
|
|
111
|
+
if (!identifier) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const raw = String(identifier);
|
|
116
|
+
const foundExact = SVS_AGENT_FRAMEWORK_ADAPTERS.find((adapter) => {
|
|
117
|
+
const values = getAdapterIdentifierValues(adapter);
|
|
118
|
+
|
|
119
|
+
return values.includes(raw);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (foundExact) {
|
|
123
|
+
return { ...foundExact };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const normalized = raw.toLowerCase();
|
|
127
|
+
const foundNormalized = SVS_AGENT_FRAMEWORK_ADAPTERS.find((adapter) => {
|
|
128
|
+
const values = getAdapterIdentifierValues(adapter).map((value) => value.toLowerCase());
|
|
129
|
+
|
|
130
|
+
return values.includes(normalized);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return foundNormalized ? { ...foundNormalized } : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getAdapterIdentifierValues(adapter) {
|
|
137
|
+
return [
|
|
138
|
+
adapter.id,
|
|
139
|
+
adapter.runtime,
|
|
140
|
+
adapter.packageExport,
|
|
141
|
+
adapter.exportPath,
|
|
142
|
+
adapter.toolName,
|
|
143
|
+
adapter.pluginName,
|
|
144
|
+
adapter.actionName
|
|
145
|
+
].filter(Boolean).map((value) => String(value));
|
|
146
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SvsAdapterActionInput,
|
|
3
|
+
SvsAdapterActionResult,
|
|
4
|
+
SvsAdapterOptions,
|
|
5
|
+
SvsAdapterReadinessResult,
|
|
6
|
+
SvsClientLike
|
|
7
|
+
} from "./common.js";
|
|
8
|
+
|
|
9
|
+
export class SvsAdapterCoreError extends Error {
|
|
10
|
+
details: unknown;
|
|
11
|
+
constructor(message: string, details?: unknown);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SvsAdapterClientOptions {
|
|
15
|
+
client?: SvsAdapterOptions["client"];
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
requestSigningSecret?: string;
|
|
19
|
+
expectedIntegrationContractHash?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SvsAdapterProductionReadinessOptions = SvsAdapterOptions & {
|
|
23
|
+
runSelfTest?: boolean;
|
|
24
|
+
pendingProofMaxAgeMs?: number;
|
|
25
|
+
requireNoExpiredPreviousSigningSecrets?: boolean;
|
|
26
|
+
readinessVersion?: string;
|
|
27
|
+
readyMessage?: string;
|
|
28
|
+
adapterLabel?: string;
|
|
29
|
+
ErrorClass?: typeof SvsAdapterCoreError;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SvsAdapterSolanaActionSubmitOptions = SvsAdapterOptions & SvsAdapterActionInput & {
|
|
33
|
+
adapterVersion: string;
|
|
34
|
+
agentFramework: string;
|
|
35
|
+
resultVersion: string;
|
|
36
|
+
readinessVersion?: string;
|
|
37
|
+
readyMessage?: string;
|
|
38
|
+
adapterLabel?: string;
|
|
39
|
+
ErrorClass?: typeof SvsAdapterCoreError;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function createSvsAdapterClient(options?: SvsAdapterClientOptions): SvsClientLike | unknown;
|
|
43
|
+
|
|
44
|
+
export function requireSvsAdapterProductionReady(options?: SvsAdapterProductionReadinessOptions): Promise<SvsAdapterReadinessResult>;
|
|
45
|
+
|
|
46
|
+
export function verifyAndSubmitSvsAdapterSolanaAction(options?: SvsAdapterSolanaActionSubmitOptions): Promise<SvsAdapterActionResult>;
|
|
47
|
+
|
|
48
|
+
export function extractSubmittedRecordId(result: unknown): string | null;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SolanaVerificationClient,
|
|
3
|
+
productionCertificationIsReady
|
|
4
|
+
} from "./index.js";
|
|
5
|
+
|
|
6
|
+
export class SvsAdapterCoreError extends Error {
|
|
7
|
+
constructor(message, details = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "SvsAdapterCoreError";
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createSvsAdapterClient({
|
|
15
|
+
client = null,
|
|
16
|
+
baseUrl,
|
|
17
|
+
apiKey,
|
|
18
|
+
requestSigningSecret,
|
|
19
|
+
expectedIntegrationContractHash
|
|
20
|
+
} = {}) {
|
|
21
|
+
return client ?? new SolanaVerificationClient({
|
|
22
|
+
baseUrl,
|
|
23
|
+
apiKey,
|
|
24
|
+
requestSigningSecret,
|
|
25
|
+
expectedIntegrationContractHash
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function requireSvsAdapterProductionReady({
|
|
30
|
+
client = null,
|
|
31
|
+
baseUrl,
|
|
32
|
+
apiKey,
|
|
33
|
+
requestSigningSecret,
|
|
34
|
+
expectedIntegrationContractHash,
|
|
35
|
+
botId,
|
|
36
|
+
certificationStaleAfterMs = undefined,
|
|
37
|
+
requireCurrentIntegrationContract = true,
|
|
38
|
+
runSelfTest = true,
|
|
39
|
+
pendingProofMaxAgeMs = undefined,
|
|
40
|
+
requireNoExpiredPreviousSigningSecrets = true,
|
|
41
|
+
readinessVersion = "svs.adapter-readiness.v1",
|
|
42
|
+
readyMessage = "SVS adapter readiness and production certification passed.",
|
|
43
|
+
adapterLabel = "SVS adapter",
|
|
44
|
+
ErrorClass = SvsAdapterCoreError
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (!botId) {
|
|
47
|
+
throw new Error("botId is required.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const svs = createSvsAdapterClient({
|
|
51
|
+
client,
|
|
52
|
+
baseUrl,
|
|
53
|
+
apiKey,
|
|
54
|
+
requestSigningSecret,
|
|
55
|
+
expectedIntegrationContractHash
|
|
56
|
+
});
|
|
57
|
+
const readiness = await svs.checkBotReadiness({
|
|
58
|
+
botId,
|
|
59
|
+
runSelfTest,
|
|
60
|
+
pendingProofMaxAgeMs,
|
|
61
|
+
requireNoExpiredPreviousSigningSecrets
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (readiness.ok !== true) {
|
|
65
|
+
throw new ErrorClass(
|
|
66
|
+
`SVS bot readiness failed for ${botId}: ${readiness.nextAction?.message ?? readiness.status ?? "not ready"}`,
|
|
67
|
+
{ readiness }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const certification = await svs.requireProductionCertification({
|
|
72
|
+
botId,
|
|
73
|
+
staleAfterMs: certificationStaleAfterMs,
|
|
74
|
+
requireCurrentIntegrationContract
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!productionCertificationIsReady(certification)) {
|
|
78
|
+
throw new ErrorClass(
|
|
79
|
+
`SVS production certification failed for ${botId}: ${certification.nextAction?.message ?? certification.status ?? "not certified"}`,
|
|
80
|
+
{
|
|
81
|
+
readiness,
|
|
82
|
+
certification
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
version: readinessVersion,
|
|
89
|
+
ok: true,
|
|
90
|
+
status: "ready",
|
|
91
|
+
botId: certification.botId ?? readiness.botId ?? botId,
|
|
92
|
+
readiness,
|
|
93
|
+
certification,
|
|
94
|
+
nextAction: {
|
|
95
|
+
code: "none",
|
|
96
|
+
message: readyMessage || `${adapterLabel} readiness and production certification passed.`
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function verifyAndSubmitSvsAdapterSolanaAction({
|
|
102
|
+
client = null,
|
|
103
|
+
baseUrl,
|
|
104
|
+
apiKey,
|
|
105
|
+
requestSigningSecret,
|
|
106
|
+
expectedIntegrationContractHash,
|
|
107
|
+
botId,
|
|
108
|
+
requestId = null,
|
|
109
|
+
idempotencyKey = requestId,
|
|
110
|
+
intent,
|
|
111
|
+
transaction = null,
|
|
112
|
+
serializedTransaction = transaction?.serializedTransaction ?? transaction,
|
|
113
|
+
policyId,
|
|
114
|
+
rpcUrl,
|
|
115
|
+
simulation = null,
|
|
116
|
+
metadata = null,
|
|
117
|
+
source = {},
|
|
118
|
+
certificationStaleAfterMs = undefined,
|
|
119
|
+
requireCurrentIntegrationContract = true,
|
|
120
|
+
waitForProof = false,
|
|
121
|
+
waitAttempts = 12,
|
|
122
|
+
waitIntervalMs = 5000,
|
|
123
|
+
fetchProof = true,
|
|
124
|
+
checkReceiptRegistryChain = true,
|
|
125
|
+
requireReadinessOnSubmit = true,
|
|
126
|
+
adapterVersion,
|
|
127
|
+
agentFramework,
|
|
128
|
+
resultVersion,
|
|
129
|
+
readinessVersion = "svs.adapter-readiness.v1",
|
|
130
|
+
readyMessage,
|
|
131
|
+
adapterLabel,
|
|
132
|
+
ErrorClass = SvsAdapterCoreError
|
|
133
|
+
} = {}) {
|
|
134
|
+
if (!botId) {
|
|
135
|
+
throw new Error("botId is required.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!intent || typeof intent !== "object") {
|
|
139
|
+
throw new Error("intent is required.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!policyId) {
|
|
143
|
+
throw new Error("policyId is required.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!serializedTransaction) {
|
|
147
|
+
throw new Error("serializedTransaction is required.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!adapterVersion) {
|
|
151
|
+
throw new Error("adapterVersion is required.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!agentFramework) {
|
|
155
|
+
throw new Error("agentFramework is required.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!resultVersion) {
|
|
159
|
+
throw new Error("resultVersion is required.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const svs = createSvsAdapterClient({
|
|
163
|
+
client,
|
|
164
|
+
baseUrl,
|
|
165
|
+
apiKey,
|
|
166
|
+
requestSigningSecret,
|
|
167
|
+
expectedIntegrationContractHash
|
|
168
|
+
});
|
|
169
|
+
let readiness = null;
|
|
170
|
+
|
|
171
|
+
if (requireReadinessOnSubmit !== false) {
|
|
172
|
+
readiness = await requireSvsAdapterProductionReady({
|
|
173
|
+
client: svs,
|
|
174
|
+
botId,
|
|
175
|
+
certificationStaleAfterMs,
|
|
176
|
+
requireCurrentIntegrationContract,
|
|
177
|
+
readinessVersion,
|
|
178
|
+
readyMessage,
|
|
179
|
+
adapterLabel,
|
|
180
|
+
ErrorClass
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const actionPayload = {
|
|
185
|
+
requestId,
|
|
186
|
+
idempotencyKey,
|
|
187
|
+
intent,
|
|
188
|
+
policyId,
|
|
189
|
+
simulation,
|
|
190
|
+
serializedTransaction,
|
|
191
|
+
rpcUrl,
|
|
192
|
+
metadata,
|
|
193
|
+
source: {
|
|
194
|
+
...source,
|
|
195
|
+
submittedBy: source.submittedBy ?? botId,
|
|
196
|
+
adapter: adapterVersion,
|
|
197
|
+
agentFramework: source.agentFramework ?? agentFramework
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const result = waitForProof
|
|
201
|
+
? await svs.submitCertifiedActionAndWaitForProof(actionPayload, {
|
|
202
|
+
idempotencyKey,
|
|
203
|
+
botId,
|
|
204
|
+
certificationStaleAfterMs,
|
|
205
|
+
requireCurrentIntegrationContract,
|
|
206
|
+
waitAttempts,
|
|
207
|
+
waitIntervalMs,
|
|
208
|
+
fetchProof,
|
|
209
|
+
checkReceiptRegistryChain
|
|
210
|
+
})
|
|
211
|
+
: await svs.submitCertifiedAction(actionPayload, {
|
|
212
|
+
idempotencyKey,
|
|
213
|
+
botId,
|
|
214
|
+
certificationStaleAfterMs,
|
|
215
|
+
requireCurrentIntegrationContract
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
version: resultVersion,
|
|
220
|
+
ok: result.ok !== false,
|
|
221
|
+
status: waitForProof ? result.status : "submitted",
|
|
222
|
+
botId: result.botId ?? botId,
|
|
223
|
+
requestId,
|
|
224
|
+
idempotencyKey,
|
|
225
|
+
policyId,
|
|
226
|
+
adapter: adapterVersion,
|
|
227
|
+
readiness,
|
|
228
|
+
result,
|
|
229
|
+
recordId: waitForProof
|
|
230
|
+
? result.recordId ?? null
|
|
231
|
+
: extractSubmittedRecordId(result),
|
|
232
|
+
productionProofReady: waitForProof
|
|
233
|
+
? result.ok === true && result.status === "production_proof_ready"
|
|
234
|
+
: false,
|
|
235
|
+
nextAction: result.nextAction ?? {
|
|
236
|
+
code: "wait_for_human_approval",
|
|
237
|
+
message: "Wait for human approval, broadcast, registry write, and production proof verification."
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function extractSubmittedRecordId(result) {
|
|
243
|
+
return result?.submitted?.record?.id ??
|
|
244
|
+
result?.submitted?.queue?.record?.id ??
|
|
245
|
+
result?.submitted?.recordId ??
|
|
246
|
+
result?.submitted?.id ??
|
|
247
|
+
result?.submitted?.action?.id ??
|
|
248
|
+
null;
|
|
249
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createHash, createPublicKey, sign, verify } from "node:crypto";
|
|
2
|
+
import { decodeBase58, encodeBase58 } from "./base58.js";
|
|
3
|
+
|
|
4
|
+
export const APPROVAL_MESSAGE_VERSION = "svs.approval.v1";
|
|
5
|
+
export const APPROVAL_MESSAGE_DOMAIN = "svs-solana-verification-system";
|
|
6
|
+
export const LEGACY_APPROVAL_MESSAGE_DOMAIN = "solana-human-verification-system";
|
|
7
|
+
|
|
8
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
9
|
+
const VALID_OUTCOMES = new Set(["approved", "rejected", "acknowledged"]);
|
|
10
|
+
const VALID_DOMAINS = new Set([APPROVAL_MESSAGE_DOMAIN, LEGACY_APPROVAL_MESSAGE_DOMAIN]);
|
|
11
|
+
|
|
12
|
+
export function createApprovalPayload(record, {
|
|
13
|
+
outcome,
|
|
14
|
+
reviewedAt,
|
|
15
|
+
domain = APPROVAL_MESSAGE_DOMAIN
|
|
16
|
+
}) {
|
|
17
|
+
assertValidOutcome(outcome);
|
|
18
|
+
assertValidDomain(domain);
|
|
19
|
+
|
|
20
|
+
if (!reviewedAt) {
|
|
21
|
+
throw new Error("reviewedAt is required to create an approval payload.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
domain,
|
|
26
|
+
version: APPROVAL_MESSAGE_VERSION,
|
|
27
|
+
chain: "solana-devnet",
|
|
28
|
+
recordId: record.id,
|
|
29
|
+
outcome,
|
|
30
|
+
controllerWallet: record.intent.controllerWallet,
|
|
31
|
+
botId: record.intent.botId,
|
|
32
|
+
txType: record.intent.txType,
|
|
33
|
+
amountSol: record.intent.amountSol,
|
|
34
|
+
destinationWallet: record.intent.destinationWallet ?? null,
|
|
35
|
+
serviceFee: record.serviceFee ?? null,
|
|
36
|
+
receiptHash: record.receipt.receiptHash,
|
|
37
|
+
policyHash: record.receipt.policyHash,
|
|
38
|
+
intentHash: record.receipt.intentHash,
|
|
39
|
+
simulationHash: record.receipt.simulationHash,
|
|
40
|
+
reviewedAt
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createApprovalMessage(record, { outcome, reviewedAt, domain }) {
|
|
45
|
+
return JSON.stringify(createApprovalPayload(record, { outcome, reviewedAt, domain }), null, 2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function signApproval({
|
|
49
|
+
record,
|
|
50
|
+
outcome,
|
|
51
|
+
reviewedAt = new Date().toISOString(),
|
|
52
|
+
wallet,
|
|
53
|
+
domain = APPROVAL_MESSAGE_DOMAIN
|
|
54
|
+
}) {
|
|
55
|
+
const message = createApprovalMessage(record, { outcome, reviewedAt, domain });
|
|
56
|
+
const signature = sign(null, Buffer.from(message, "utf8"), wallet.privateKey);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
version: APPROVAL_MESSAGE_VERSION,
|
|
60
|
+
scheme: "ed25519",
|
|
61
|
+
signer: wallet.publicKey,
|
|
62
|
+
message,
|
|
63
|
+
messageHash: hashMessage(message),
|
|
64
|
+
signature: encodeBase58(signature)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function verifyApprovalSignature(record, signedApproval) {
|
|
69
|
+
if (!signedApproval || typeof signedApproval !== "object") {
|
|
70
|
+
throw new Error("A signed approval is required.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const payload = parseApprovalMessage(signedApproval.message);
|
|
74
|
+
const expectedMessage = createApprovalMessage(record, {
|
|
75
|
+
outcome: payload.outcome,
|
|
76
|
+
reviewedAt: payload.reviewedAt,
|
|
77
|
+
domain: payload.domain
|
|
78
|
+
});
|
|
79
|
+
const signer = signedApproval.signer ?? payload.controllerWallet;
|
|
80
|
+
|
|
81
|
+
if (signedApproval.version && signedApproval.version !== APPROVAL_MESSAGE_VERSION) {
|
|
82
|
+
throw new Error(`Unsupported approval version: ${signedApproval.version}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (signedApproval.scheme && signedApproval.scheme !== "ed25519") {
|
|
86
|
+
throw new Error(`Unsupported approval signature scheme: ${signedApproval.scheme}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (signedApproval.message !== expectedMessage) {
|
|
90
|
+
throw new Error("Signed approval message does not match this approval record.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (signedApproval.messageHash && signedApproval.messageHash !== hashMessage(signedApproval.message)) {
|
|
94
|
+
throw new Error("Signed approval message hash does not match the approval message.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (signer !== record.intent.controllerWallet) {
|
|
98
|
+
throw new Error("Signed approval must come from the controller wallet.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const publicKey = createSolanaPublicKey(signer);
|
|
102
|
+
const signature = decodeBase58(signedApproval.signature);
|
|
103
|
+
const verified = verify(null, Buffer.from(signedApproval.message, "utf8"), publicKey, signature);
|
|
104
|
+
|
|
105
|
+
if (!verified) {
|
|
106
|
+
throw new Error("Approval signature verification failed.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
version: APPROVAL_MESSAGE_VERSION,
|
|
111
|
+
scheme: "ed25519",
|
|
112
|
+
signer,
|
|
113
|
+
message: signedApproval.message,
|
|
114
|
+
messageHash: hashMessage(signedApproval.message),
|
|
115
|
+
signature: signedApproval.signature,
|
|
116
|
+
verified: true,
|
|
117
|
+
payload
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function summarizeApprovalDomain(signatureOrPayload) {
|
|
122
|
+
const domain = extractApprovalDomain(signatureOrPayload);
|
|
123
|
+
const status = domain === APPROVAL_MESSAGE_DOMAIN
|
|
124
|
+
? "current"
|
|
125
|
+
: domain === LEGACY_APPROVAL_MESSAGE_DOMAIN
|
|
126
|
+
? "legacy_compatible"
|
|
127
|
+
: domain
|
|
128
|
+
? "unsupported"
|
|
129
|
+
: "missing";
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
domain,
|
|
133
|
+
status,
|
|
134
|
+
current: status === "current",
|
|
135
|
+
legacyCompatible: status === "legacy_compatible",
|
|
136
|
+
supported: status === "current" || status === "legacy_compatible"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseApprovalMessage(message) {
|
|
141
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
142
|
+
throw new Error("Signed approval message is required.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const payload = JSON.parse(message);
|
|
146
|
+
|
|
147
|
+
if (payload.version !== APPROVAL_MESSAGE_VERSION) {
|
|
148
|
+
throw new Error(`Unsupported approval message version: ${payload.version}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
assertValidOutcome(payload.outcome);
|
|
152
|
+
assertValidDomain(payload.domain);
|
|
153
|
+
|
|
154
|
+
return payload;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function extractApprovalDomain(signatureOrPayload) {
|
|
158
|
+
if (!signatureOrPayload || typeof signatureOrPayload !== "object") return null;
|
|
159
|
+
if (signatureOrPayload.domain) return signatureOrPayload.domain;
|
|
160
|
+
if (!signatureOrPayload.message) return null;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(signatureOrPayload.message)?.domain ?? null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createSolanaPublicKey(publicKey) {
|
|
170
|
+
const publicKeyBytes = decodeBase58(publicKey);
|
|
171
|
+
|
|
172
|
+
if (publicKeyBytes.length !== 32) {
|
|
173
|
+
throw new Error("Controller wallet public key must decode to 32 bytes.");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return createPublicKey({
|
|
177
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(publicKeyBytes)]),
|
|
178
|
+
format: "der",
|
|
179
|
+
type: "spki"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function hashMessage(message) {
|
|
184
|
+
return createHash("sha256").update(message).digest("hex");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function assertValidOutcome(outcome) {
|
|
188
|
+
if (!VALID_OUTCOMES.has(outcome)) {
|
|
189
|
+
throw new Error("Review outcome must be approved, rejected, or acknowledged.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function assertValidDomain(domain) {
|
|
194
|
+
if (!VALID_DOMAINS.has(domain)) {
|
|
195
|
+
throw new Error(`Unsupported approval message domain: ${domain ?? "missing"}.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
package/dist/base58.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
2
|
+
const BASE = BigInt(ALPHABET.length);
|
|
3
|
+
const LOOKUP = new Map([...ALPHABET].map((char, index) => [char, BigInt(index)]));
|
|
4
|
+
|
|
5
|
+
export function encodeBase58(bytes) {
|
|
6
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
7
|
+
throw new TypeError("encodeBase58 expects a Uint8Array.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let value = 0n;
|
|
11
|
+
|
|
12
|
+
for (const byte of bytes) {
|
|
13
|
+
value = value * 256n + BigInt(byte);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let encoded = "";
|
|
17
|
+
|
|
18
|
+
while (value > 0n) {
|
|
19
|
+
const remainder = value % BASE;
|
|
20
|
+
value /= BASE;
|
|
21
|
+
encoded = ALPHABET[Number(remainder)] + encoded;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const byte of bytes) {
|
|
25
|
+
if (byte !== 0) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
encoded = "1" + encoded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return encoded || "1";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function decodeBase58(value) {
|
|
36
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
37
|
+
throw new TypeError("decodeBase58 expects a non-empty string.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let decoded = 0n;
|
|
41
|
+
|
|
42
|
+
for (const char of value) {
|
|
43
|
+
const index = LOOKUP.get(char);
|
|
44
|
+
|
|
45
|
+
if (index === undefined) {
|
|
46
|
+
throw new Error(`Invalid base58 character: ${char}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
decoded = decoded * BASE + index;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const bytes = [];
|
|
53
|
+
|
|
54
|
+
while (decoded > 0n) {
|
|
55
|
+
bytes.unshift(Number(decoded % 256n));
|
|
56
|
+
decoded /= 256n;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const char of value) {
|
|
60
|
+
if (char !== "1") {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
bytes.unshift(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Uint8Array.from(bytes);
|
|
68
|
+
}
|
|
69
|
+
|