@propxchain/core-client 0.3.0-canary.27 → 0.3.0-canary.29
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/dist/audit/auditChain.d.ts +20 -0
- package/dist/audit/auditChain.d.ts.map +1 -0
- package/dist/audit/auditChain.js +67 -0
- package/dist/audit/auditChain.js.map +1 -0
- package/dist/audit/index.d.ts +4 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/sealAuditBundle.d.ts +36 -0
- package/dist/audit/sealAuditBundle.d.ts.map +1 -0
- package/dist/audit/sealAuditBundle.js +58 -0
- package/dist/audit/sealAuditBundle.js.map +1 -0
- package/dist/audit/verifyAuditLog.d.ts +28 -0
- package/dist/audit/verifyAuditLog.d.ts.map +1 -0
- package/dist/audit/verifyAuditLog.js +106 -0
- package/dist/audit/verifyAuditLog.js.map +1 -0
- package/dist/canisters/transaction_manager/transaction_manager.did.d.ts.map +1 -1
- package/dist/canisters/transaction_manager/transaction_manager.did.js +21 -4
- package/dist/canisters/transaction_manager/transaction_manager.did.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/audit/auditChain.ts +88 -0
- package/src/audit/index.ts +19 -0
- package/src/audit/sealAuditBundle.ts +90 -0
- package/src/audit/verifyAuditLog.ts +168 -0
- package/src/canisters/transaction_manager/transaction_manager.did +30 -10
- package/src/canisters/transaction_manager/transaction_manager.did.d.ts +19 -8
- package/src/canisters/transaction_manager/transaction_manager.did.js +25 -4
- package/src/index.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@propxchain/core-client",
|
|
3
|
-
"version": "0.3.0-canary.
|
|
3
|
+
"version": "0.3.0-canary.29",
|
|
4
4
|
"description": "Typed TypeScript client for the PropXchain core canisters on the Internet Computer. Includes a deterministic mock mode for local development and tests.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@dfinity/agent": "^1.4.0",
|
|
39
|
+
"@dfinity/certificate-verification": "^3.1.0",
|
|
40
|
+
"@dfinity/principal": "^1.4.0",
|
|
38
41
|
"@icp-sdk/auth": "^7.0.0",
|
|
39
42
|
"@icp-sdk/core": "^5.4.0"
|
|
40
43
|
},
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Audit hash chain — TypeScript mirror of ledger_manager's derived chain (ADR 0013).
|
|
2
|
+
//
|
|
3
|
+
// This MUST match the Motoko encoding in
|
|
4
|
+
// packages/core/src/ledger_manager/main.mo (encodeAuditEvent / chainHead)
|
|
5
|
+
// byte-for-byte, or verification will reject genuine records. The canonical
|
|
6
|
+
// encoding: each field 4-byte big-endian length-prefixed UTF-8; metadata uses
|
|
7
|
+
// a 1-byte presence tag (0 = null, 1 = present). eventHash_i =
|
|
8
|
+
// SHA-256(prevHash ++ encode(event_i)); genesis prevHash = 32 zero bytes;
|
|
9
|
+
// events ordered by eventId ascending.
|
|
10
|
+
|
|
11
|
+
import { Principal } from "@icp-sdk/core/principal";
|
|
12
|
+
|
|
13
|
+
/** Structural shape of a decoded ledger_manager AuditEvent (candid). */
|
|
14
|
+
export interface CertifiedAuditEvent {
|
|
15
|
+
eventId: bigint;
|
|
16
|
+
transactionId: string;
|
|
17
|
+
eventType: string;
|
|
18
|
+
timestamp: bigint;
|
|
19
|
+
caller: Principal;
|
|
20
|
+
details: string;
|
|
21
|
+
metadata: [] | [string];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
|
|
26
|
+
function lenPrefixed(bytes: Uint8Array): Uint8Array {
|
|
27
|
+
const n = bytes.length;
|
|
28
|
+
const out = new Uint8Array(4 + n);
|
|
29
|
+
out[0] = (n >>> 24) & 0xff;
|
|
30
|
+
out[1] = (n >>> 16) & 0xff;
|
|
31
|
+
out[2] = (n >>> 8) & 0xff;
|
|
32
|
+
out[3] = n & 0xff;
|
|
33
|
+
out.set(bytes, 4);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function concat(chunks: Uint8Array[]): Uint8Array {
|
|
38
|
+
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
39
|
+
const out = new Uint8Array(total);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (const c of chunks) {
|
|
42
|
+
out.set(c, offset);
|
|
43
|
+
offset += c.length;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Canonical event encoding — mirrors ledger_manager.encodeAuditEvent. */
|
|
49
|
+
export function encodeAuditEvent(e: CertifiedAuditEvent): Uint8Array {
|
|
50
|
+
const parts: Uint8Array[] = [
|
|
51
|
+
lenPrefixed(encoder.encode(e.eventId.toString())),
|
|
52
|
+
lenPrefixed(encoder.encode(e.transactionId)),
|
|
53
|
+
lenPrefixed(encoder.encode(e.eventType)),
|
|
54
|
+
lenPrefixed(encoder.encode(e.timestamp.toString())),
|
|
55
|
+
lenPrefixed(encoder.encode(e.caller.toText())),
|
|
56
|
+
lenPrefixed(encoder.encode(e.details)),
|
|
57
|
+
];
|
|
58
|
+
if (e.metadata.length === 0) {
|
|
59
|
+
parts.push(new Uint8Array([0]));
|
|
60
|
+
} else {
|
|
61
|
+
parts.push(new Uint8Array([1]));
|
|
62
|
+
parts.push(lenPrefixed(encoder.encode(e.metadata[0])));
|
|
63
|
+
}
|
|
64
|
+
return concat(parts);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function sha256(data: Uint8Array): Promise<Uint8Array> {
|
|
68
|
+
const digest = await crypto.subtle.digest("SHA-256", data as unknown as BufferSource);
|
|
69
|
+
return new Uint8Array(digest);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Recompute a transaction's chain head from its events — mirrors
|
|
74
|
+
* ledger_manager.chainHead. Events are sorted by eventId ascending (the
|
|
75
|
+
* canister's canonical order) so the result is independent of array order.
|
|
76
|
+
*/
|
|
77
|
+
export async function recomputeHeadHash(
|
|
78
|
+
events: CertifiedAuditEvent[],
|
|
79
|
+
): Promise<Uint8Array> {
|
|
80
|
+
const ordered = [...events].sort((a, b) =>
|
|
81
|
+
a.eventId < b.eventId ? -1 : a.eventId > b.eventId ? 1 : 0,
|
|
82
|
+
);
|
|
83
|
+
let head: Uint8Array = new Uint8Array(32); // genesis: 32 zero bytes
|
|
84
|
+
for (const e of ordered) {
|
|
85
|
+
head = await sha256(concat([head, encodeAuditEvent(e)]));
|
|
86
|
+
}
|
|
87
|
+
return head;
|
|
88
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Verifiable audit trail (ADR 0013) — chain recompute, certificate
|
|
2
|
+
// verification, and sealed proof bundles for the per-transaction audit log.
|
|
3
|
+
export {
|
|
4
|
+
type CertifiedAuditEvent,
|
|
5
|
+
encodeAuditEvent,
|
|
6
|
+
recomputeHeadHash,
|
|
7
|
+
} from "./auditChain.js";
|
|
8
|
+
export {
|
|
9
|
+
type CertifiedTransactionLog,
|
|
10
|
+
type VerifyResult,
|
|
11
|
+
type VerifyOptions,
|
|
12
|
+
verifyCertifiedAuditLog,
|
|
13
|
+
} from "./verifyAuditLog.js";
|
|
14
|
+
export {
|
|
15
|
+
AUDIT_SEAL_DOC_TYPE,
|
|
16
|
+
type AuditSealBundle,
|
|
17
|
+
buildAuditSealBundle,
|
|
18
|
+
serializeAuditSealBundle,
|
|
19
|
+
} from "./sealAuditBundle.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Sealed audit bundle (ADR 0013, proof model ii).
|
|
2
|
+
//
|
|
3
|
+
// A contemporaneous, self-contained snapshot of a transaction's certified
|
|
4
|
+
// audit log. Built from a getCertifiedTransactionLog response (a query, so the
|
|
5
|
+
// subnet certificate is present) — typically at completion, and regenerable on
|
|
6
|
+
// demand. The bundle verifies offline against the IC root key, even if
|
|
7
|
+
// PropXchain is gone: a forensic verifier re-runs verifyCertifiedAuditLog with
|
|
8
|
+
// a relaxed freshness window.
|
|
9
|
+
//
|
|
10
|
+
// Persistence: the serialized bundle is stored on-chain in document_storage
|
|
11
|
+
// (docType "audit_seal") per ADR 0008. That upload uses the existing document
|
|
12
|
+
// upload path and is the caller's step — buildAuditSealBundle only produces the
|
|
13
|
+
// bytes (no document bodies are embedded; events reference any documents by
|
|
14
|
+
// hash only).
|
|
15
|
+
|
|
16
|
+
import { type CertifiedTransactionLog } from "./verifyAuditLog.js";
|
|
17
|
+
|
|
18
|
+
export const AUDIT_SEAL_DOC_TYPE = "audit_seal";
|
|
19
|
+
|
|
20
|
+
interface SerializableAuditEvent {
|
|
21
|
+
eventId: string;
|
|
22
|
+
transactionId: string;
|
|
23
|
+
eventType: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
caller: string;
|
|
26
|
+
details: string;
|
|
27
|
+
metadata: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuditSealBundle {
|
|
31
|
+
version: 1;
|
|
32
|
+
canisterId: string;
|
|
33
|
+
transactionId: string;
|
|
34
|
+
/** Client capture time (ISO). The authoritative time is inside the certificate. */
|
|
35
|
+
capturedAt: string;
|
|
36
|
+
events: SerializableAuditEvent[];
|
|
37
|
+
headHashHex: string;
|
|
38
|
+
certificateB64: string;
|
|
39
|
+
witnessB64: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toHex(bytes: Uint8Array): string {
|
|
43
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toB64(bytes: Uint8Array): string {
|
|
47
|
+
// Browser + Node (>=20) both expose btoa; build a binary string first.
|
|
48
|
+
let binary = "";
|
|
49
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
50
|
+
return btoa(binary);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a sealed, serializable proof bundle from a certified log response.
|
|
55
|
+
* Throws if the response carries no certificate (nothing to seal).
|
|
56
|
+
*/
|
|
57
|
+
export function buildAuditSealBundle(args: {
|
|
58
|
+
canisterId: string;
|
|
59
|
+
transactionId: string;
|
|
60
|
+
capturedAt: string;
|
|
61
|
+
log: CertifiedTransactionLog;
|
|
62
|
+
}): AuditSealBundle {
|
|
63
|
+
const { canisterId, transactionId, capturedAt, log } = args;
|
|
64
|
+
if (log.certificate.length === 0) {
|
|
65
|
+
throw new Error("cannot seal: certified log response has no certificate");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
canisterId,
|
|
70
|
+
transactionId,
|
|
71
|
+
capturedAt,
|
|
72
|
+
events: log.events.map((e) => ({
|
|
73
|
+
eventId: e.eventId.toString(),
|
|
74
|
+
transactionId: e.transactionId,
|
|
75
|
+
eventType: e.eventType,
|
|
76
|
+
timestamp: e.timestamp.toString(),
|
|
77
|
+
caller: e.caller.toText(),
|
|
78
|
+
details: e.details,
|
|
79
|
+
metadata: e.metadata.length === 0 ? null : e.metadata[0],
|
|
80
|
+
})),
|
|
81
|
+
headHashHex: toHex(log.headHash),
|
|
82
|
+
certificateB64: toB64(log.certificate[0]),
|
|
83
|
+
witnessB64: toB64(log.witness),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Serialize a bundle to bytes for on-chain storage (document_storage). */
|
|
88
|
+
export function serializeAuditSealBundle(bundle: AuditSealBundle): Uint8Array {
|
|
89
|
+
return new TextEncoder().encode(JSON.stringify(bundle));
|
|
90
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Verifier for the certified per-transaction audit log (ADR 0013).
|
|
2
|
+
//
|
|
3
|
+
// Proves a transaction's audit trail is genuine canister state against the IC
|
|
4
|
+
// root key, trusting no part of PropXchain:
|
|
5
|
+
// 1. verify the subnet certificate + Merkle witness;
|
|
6
|
+
// 2. read the certified headHash for the transaction from the witness tree;
|
|
7
|
+
// 3. confirm the response's stated head matches the certified head;
|
|
8
|
+
// 4. recompute the chain from the events and confirm it matches.
|
|
9
|
+
//
|
|
10
|
+
// Uses the @dfinity/* family (agent, principal, certificate-verification)
|
|
11
|
+
// throughout — that is the lineage certificate-verification depends on, so the
|
|
12
|
+
// HashTree/Principal types line up. The rest of core-client uses @icp-sdk;
|
|
13
|
+
// this leaf module's boundary is plain Uint8Array/string, so the split is
|
|
14
|
+
// contained.
|
|
15
|
+
//
|
|
16
|
+
// Live verification keeps the certificate-freshness window tight. Forensic
|
|
17
|
+
// verification of a sealed historical bundle (ADR 0013, proof model ii)
|
|
18
|
+
// relaxes it by design — the bundle proves a fact as-of the certificate's
|
|
19
|
+
// /time, not a current one.
|
|
20
|
+
|
|
21
|
+
import { lookup_path, lookupResultToBuffer } from "@dfinity/agent";
|
|
22
|
+
import type { HashTree } from "@dfinity/agent";
|
|
23
|
+
import { Principal } from "@dfinity/principal";
|
|
24
|
+
import { type CertifiedAuditEvent, recomputeHeadHash } from "./auditChain.js";
|
|
25
|
+
|
|
26
|
+
// @dfinity/certificate-verification ships an ESM .d.ts (named exports, no
|
|
27
|
+
// default) but a UMD .cjs as `main`. TS sees ESM; Node's loader sees CJS. A
|
|
28
|
+
// static named import typechecks but fails at runtime; a default import does
|
|
29
|
+
// the reverse. Load it dynamically and fall back across both shapes so the
|
|
30
|
+
// verifier works in Node, browser bundlers, and typecheck alike.
|
|
31
|
+
interface VerifyCertificationParams {
|
|
32
|
+
canisterId: Principal;
|
|
33
|
+
encodedCertificate: ArrayBuffer;
|
|
34
|
+
encodedTree: ArrayBuffer;
|
|
35
|
+
rootKey: ArrayBuffer;
|
|
36
|
+
maxCertificateTimeOffsetMs: number;
|
|
37
|
+
}
|
|
38
|
+
type VerifyCertificationFn = (p: VerifyCertificationParams) => Promise<HashTree>;
|
|
39
|
+
|
|
40
|
+
let cachedVerify: VerifyCertificationFn | undefined;
|
|
41
|
+
async function loadVerifyCertification(): Promise<VerifyCertificationFn> {
|
|
42
|
+
if (cachedVerify) return cachedVerify;
|
|
43
|
+
const mod = (await import("@dfinity/certificate-verification")) as unknown as {
|
|
44
|
+
verifyCertification?: VerifyCertificationFn;
|
|
45
|
+
default?: { verifyCertification?: VerifyCertificationFn };
|
|
46
|
+
};
|
|
47
|
+
const fn = mod.verifyCertification ?? mod.default?.verifyCertification;
|
|
48
|
+
if (!fn) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"verifyCertification not found in @dfinity/certificate-verification",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
cachedVerify = fn;
|
|
54
|
+
return fn;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Decoded ledger_manager.getCertifiedTransactionLog response. */
|
|
58
|
+
export interface CertifiedTransactionLog {
|
|
59
|
+
events: CertifiedAuditEvent[];
|
|
60
|
+
headHash: Uint8Array;
|
|
61
|
+
certificate: [] | [Uint8Array];
|
|
62
|
+
witness: Uint8Array;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface VerifyResult {
|
|
66
|
+
verified: boolean;
|
|
67
|
+
reason?: string;
|
|
68
|
+
eventCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface VerifyOptions {
|
|
72
|
+
canisterId: string;
|
|
73
|
+
/** IC root public key — the mainnet constant, or `agent.rootKey` for local. */
|
|
74
|
+
rootKey: Uint8Array;
|
|
75
|
+
transactionId: string;
|
|
76
|
+
log: CertifiedTransactionLog;
|
|
77
|
+
/**
|
|
78
|
+
* Certificate freshness window. Omit for live verification (5 min). For
|
|
79
|
+
* forensic verification of a sealed bundle, pass a large value (e.g.
|
|
80
|
+
* Number.MAX_SAFE_INTEGER) to accept a historical certificate.
|
|
81
|
+
*/
|
|
82
|
+
maxCertificateTimeOffsetMs?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
86
|
+
|
|
87
|
+
function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
|
|
88
|
+
const copy = new Uint8Array(u8.length);
|
|
89
|
+
copy.set(u8);
|
|
90
|
+
return copy.buffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
94
|
+
if (a.length !== b.length) return false;
|
|
95
|
+
let diff = 0;
|
|
96
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
97
|
+
return diff === 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function verifyCertifiedAuditLog(
|
|
101
|
+
opts: VerifyOptions,
|
|
102
|
+
): Promise<VerifyResult> {
|
|
103
|
+
const { log, transactionId } = opts;
|
|
104
|
+
const eventCount = log.events.length;
|
|
105
|
+
|
|
106
|
+
if (log.certificate.length === 0) {
|
|
107
|
+
return { verified: false, reason: "no certificate in response", eventCount };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 1. Verify the subnet certificate + witness against the IC root key.
|
|
111
|
+
let tree: HashTree;
|
|
112
|
+
try {
|
|
113
|
+
const verifyCertification = await loadVerifyCertification();
|
|
114
|
+
tree = await verifyCertification({
|
|
115
|
+
canisterId: Principal.fromText(opts.canisterId),
|
|
116
|
+
encodedCertificate: toArrayBuffer(log.certificate[0]),
|
|
117
|
+
encodedTree: toArrayBuffer(log.witness),
|
|
118
|
+
rootKey: toArrayBuffer(opts.rootKey),
|
|
119
|
+
maxCertificateTimeOffsetMs:
|
|
120
|
+
opts.maxCertificateTimeOffsetMs ?? FIVE_MINUTES_MS,
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return {
|
|
124
|
+
verified: false,
|
|
125
|
+
reason: `certificate verification failed: ${String(err)}`,
|
|
126
|
+
eventCount,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Read the certified headHash for this transaction from the witness tree.
|
|
131
|
+
// The path segment is the UTF-8 bytes of the transactionId — exactly the key
|
|
132
|
+
// ledger_manager certified (Text.encodeUtf8(transactionId)).
|
|
133
|
+
const lookup = lookup_path(
|
|
134
|
+
[toArrayBuffer(new TextEncoder().encode(transactionId))],
|
|
135
|
+
tree,
|
|
136
|
+
);
|
|
137
|
+
const certifiedBuf = lookupResultToBuffer(lookup);
|
|
138
|
+
if (!certifiedBuf) {
|
|
139
|
+
return {
|
|
140
|
+
verified: false,
|
|
141
|
+
reason: "transaction head not present in certified tree",
|
|
142
|
+
eventCount,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const certifiedHead = new Uint8Array(certifiedBuf);
|
|
146
|
+
|
|
147
|
+
// 3. The response's stated head must match the certified value.
|
|
148
|
+
if (!equalBytes(certifiedHead, log.headHash)) {
|
|
149
|
+
return {
|
|
150
|
+
verified: false,
|
|
151
|
+
reason: "response headHash does not match certified value",
|
|
152
|
+
eventCount,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. The head recomputed from the events must match the certified head.
|
|
157
|
+
const recomputed = await recomputeHeadHash(log.events);
|
|
158
|
+
if (!equalBytes(recomputed, certifiedHead)) {
|
|
159
|
+
return {
|
|
160
|
+
verified: false,
|
|
161
|
+
reason:
|
|
162
|
+
"recomputed chain head does not match certified head — events tampered",
|
|
163
|
+
eventCount,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { verified: true, eventCount };
|
|
168
|
+
}
|
|
@@ -272,12 +272,15 @@ type Result_3 =
|
|
|
272
272
|
err: text;
|
|
273
273
|
ok: OfficialSearchResult;
|
|
274
274
|
};
|
|
275
|
-
type
|
|
275
|
+
type Result_21 =
|
|
276
276
|
variant {
|
|
277
277
|
err: text;
|
|
278
|
-
ok:
|
|
278
|
+
ok: record {
|
|
279
|
+
registrations: nat;
|
|
280
|
+
transactions: nat;
|
|
281
|
+
};
|
|
279
282
|
};
|
|
280
|
-
type
|
|
283
|
+
type Result_20 =
|
|
281
284
|
variant {
|
|
282
285
|
err: text;
|
|
283
286
|
ok: record {
|
|
@@ -285,11 +288,21 @@ type Result_19 =
|
|
|
285
288
|
text;
|
|
286
289
|
};
|
|
287
290
|
};
|
|
288
|
-
type
|
|
291
|
+
type Result_2 =
|
|
292
|
+
variant {
|
|
293
|
+
err: text;
|
|
294
|
+
ok: TransactionParty;
|
|
295
|
+
};
|
|
296
|
+
type Result_19 =
|
|
289
297
|
variant {
|
|
290
298
|
err: text;
|
|
291
299
|
ok: Phase;
|
|
292
300
|
};
|
|
301
|
+
type Result_18 =
|
|
302
|
+
variant {
|
|
303
|
+
err: text;
|
|
304
|
+
ok: opt HmlrFetchRecord;
|
|
305
|
+
};
|
|
293
306
|
type Result_17 =
|
|
294
307
|
variant {
|
|
295
308
|
err: text;
|
|
@@ -468,6 +481,13 @@ type LandRegistryIntegration =
|
|
|
468
481
|
submittedToLRAt: opt int;
|
|
469
482
|
totalLRCosts: nat;
|
|
470
483
|
};
|
|
484
|
+
type HmlrFetchRecord =
|
|
485
|
+
record {
|
|
486
|
+
fetchedAt: int;
|
|
487
|
+
fetchedBy: principal;
|
|
488
|
+
responseHash: text;
|
|
489
|
+
titleNumber: text;
|
|
490
|
+
};
|
|
471
491
|
type BotConnection =
|
|
472
492
|
record {
|
|
473
493
|
addedAt: int;
|
|
@@ -500,6 +520,7 @@ service : {
|
|
|
500
520
|
(Result);
|
|
501
521
|
assignSolicitorRecord: (transactionId: text, solicitorRecord:
|
|
502
522
|
SolicitorRecord) -> (Result_1);
|
|
523
|
+
backfillTransactionMembers: () -> (Result_21);
|
|
503
524
|
bootstrapDocumentStorageCanister: (canisterId: principal) -> (Result);
|
|
504
525
|
canAccessTransaction: (transactionId: text) -> (bool) query;
|
|
505
526
|
connectBot: (transactionId: text, botPrincipal: principal, botName:
|
|
@@ -519,7 +540,7 @@ service : {
|
|
|
519
540
|
postcode: text, titleNumber: text, seller: principal, previousOwner:
|
|
520
541
|
text, amount: nat64, transactionType: text, userRole: text, propertyType:
|
|
521
542
|
text, propertyCategory: text, mode: text, deposit: nat64, mortgageAmount:
|
|
522
|
-
nat64, completionDate: text) -> (
|
|
543
|
+
nat64, completionDate: text) -> (Result_20);
|
|
523
544
|
deleteTransaction: (transactionId: text) -> (Result);
|
|
524
545
|
disconnectBot: (transactionId: text, botPrincipal: principal) -> (Result_1);
|
|
525
546
|
doesTransactionExist: (transactionId: text) -> (bool) query;
|
|
@@ -528,11 +549,12 @@ service : {
|
|
|
528
549
|
text;
|
|
529
550
|
}) query;
|
|
530
551
|
getAllTransactions: () -> (vec Transaction) query;
|
|
531
|
-
getCurrentPhase: (transactionId: text) -> (
|
|
552
|
+
getCurrentPhase: (transactionId: text) -> (Result_19) query;
|
|
532
553
|
getCycles: () -> (nat) query;
|
|
533
554
|
getDocumentStorageCanister: () -> (opt principal) query;
|
|
534
555
|
getExpiringSearches: (withinDays: nat) -> (vec Transaction) query;
|
|
535
556
|
getFlowState: (transactionId: text) -> (opt text) query;
|
|
557
|
+
getHmlrFetch: (transactionId: text) -> (Result_18) query;
|
|
536
558
|
getInviteCode: (transactionId: text) -> (Result) query;
|
|
537
559
|
getLandRegistryCanister: () -> (text) query;
|
|
538
560
|
getLandRegistryDetail: (transactionID: text) ->
|
|
@@ -580,7 +602,6 @@ service : {
|
|
|
580
602
|
/// create. Counterpart to `removeParty` (which is seller/admin-driven and
|
|
581
603
|
/// gated against primary parties) — this is the "I want out" path for a
|
|
582
604
|
/// joined buyer or any other access-list member.
|
|
583
|
-
///
|
|
584
605
|
/// Gates:
|
|
585
606
|
/// - Anonymous callers rejected outright.
|
|
586
607
|
/// - Seller / createdBy cannot leave their own transaction (they
|
|
@@ -588,7 +609,6 @@ service : {
|
|
|
588
609
|
/// - Caller must currently have access to the transaction.
|
|
589
610
|
/// - Locked after contract exchange (same posture as assignBuyer):
|
|
590
611
|
/// once the deal is signed nobody walks away by toggling a button.
|
|
591
|
-
///
|
|
592
612
|
/// Effects:
|
|
593
613
|
/// - Caller is removed from `accessList`.
|
|
594
614
|
/// - If the caller is the assigned buyer (`txn.buyer == msg.caller`),
|
|
@@ -598,7 +618,6 @@ service : {
|
|
|
598
618
|
/// reusable by a new joiner.
|
|
599
619
|
/// - Audit event `buyer_left` (or `party_left` for non-buyer leavers)
|
|
600
620
|
/// is logged via the existing ledger pattern.
|
|
601
|
-
///
|
|
602
621
|
/// Concurrency: uses the same acquireTxLock/finally releaseTxLock
|
|
603
622
|
/// posture as assignBuyer so a leave can't race against an in-flight
|
|
604
623
|
/// stage write.
|
|
@@ -616,7 +635,6 @@ service : {
|
|
|
616
635
|
/// 2. Stale-pin txs where the org's pinned principal changed after the
|
|
617
636
|
/// tx was created (e.g. admin password reset → new ICP key, or org
|
|
618
637
|
/// ownership transferred to a different admin).
|
|
619
|
-
///
|
|
620
638
|
/// Idempotent over the target: re-running with the same developerPrincipal
|
|
621
639
|
/// returns an error (already at target), so admin-driven backfill scripts
|
|
622
640
|
/// don't silently re-touch state. Always rejects:
|
|
@@ -638,6 +656,8 @@ service : {
|
|
|
638
656
|
acceptedQuoteHash: text, finalAmountPence: nat) -> (Result_1);
|
|
639
657
|
recordFormUpload: (txId: text, formType: text, fileHash: text, fileName:
|
|
640
658
|
text) -> (Result_1);
|
|
659
|
+
recordHmlrFetched: (transactionId: text, titleNumber: text, responseHash:
|
|
660
|
+
text) -> (Result_1);
|
|
641
661
|
/// Record a single party's signature on-chain.
|
|
642
662
|
/// The canister identifies the caller as buyer or seller from the transaction principals.
|
|
643
663
|
/// When both parties have signed, the status auto-advances to #exchanged.
|
|
@@ -20,6 +20,12 @@ export interface BotConnection {
|
|
|
20
20
|
'addedAt' : bigint,
|
|
21
21
|
'addedBy' : Principal,
|
|
22
22
|
}
|
|
23
|
+
export interface HmlrFetchRecord {
|
|
24
|
+
'fetchedAt' : bigint,
|
|
25
|
+
'fetchedBy' : Principal,
|
|
26
|
+
'titleNumber' : string,
|
|
27
|
+
'responseHash' : string,
|
|
28
|
+
}
|
|
23
29
|
export interface LandRegistryIntegration {
|
|
24
30
|
'status' : LandRegistryStatus,
|
|
25
31
|
'lastPolledAt' : [] | [bigint],
|
|
@@ -152,12 +158,18 @@ export type Result_16 = { 'ok' : TransactionPartyProgress } |
|
|
|
152
158
|
{ 'err' : string };
|
|
153
159
|
export type Result_17 = { 'ok' : NextStepRecommendation } |
|
|
154
160
|
{ 'err' : string };
|
|
155
|
-
export type Result_18 = { 'ok' :
|
|
161
|
+
export type Result_18 = { 'ok' : [] | [HmlrFetchRecord] } |
|
|
156
162
|
{ 'err' : string };
|
|
157
|
-
export type Result_19 = { 'ok' :
|
|
163
|
+
export type Result_19 = { 'ok' : Phase } |
|
|
158
164
|
{ 'err' : string };
|
|
159
165
|
export type Result_2 = { 'ok' : TransactionParty } |
|
|
160
166
|
{ 'err' : string };
|
|
167
|
+
export type Result_20 = { 'ok' : [string, string] } |
|
|
168
|
+
{ 'err' : string };
|
|
169
|
+
export type Result_21 = {
|
|
170
|
+
'ok' : { 'registrations' : bigint, 'transactions' : bigint }
|
|
171
|
+
} |
|
|
172
|
+
{ 'err' : string };
|
|
161
173
|
export type Result_3 = { 'ok' : OfficialSearchResult } |
|
|
162
174
|
{ 'err' : string };
|
|
163
175
|
export type Result_4 = { 'ok' : ApplicationStatusUpdate } |
|
|
@@ -380,6 +392,7 @@ export interface _SERVICE {
|
|
|
380
392
|
'assignBuyer' : ActorMethod<[string, Principal], Result>,
|
|
381
393
|
'assignSolicitor' : ActorMethod<[string, Principal], Result>,
|
|
382
394
|
'assignSolicitorRecord' : ActorMethod<[string, SolicitorRecord], Result_1>,
|
|
395
|
+
'backfillTransactionMembers' : ActorMethod<[], Result_21>,
|
|
383
396
|
'bootstrapDocumentStorageCanister' : ActorMethod<[Principal], Result>,
|
|
384
397
|
'canAccessTransaction' : ActorMethod<[string], boolean>,
|
|
385
398
|
'connectBot' : ActorMethod<[string, Principal, string], Result_1>,
|
|
@@ -425,18 +438,19 @@ export interface _SERVICE {
|
|
|
425
438
|
bigint,
|
|
426
439
|
string,
|
|
427
440
|
],
|
|
428
|
-
|
|
441
|
+
Result_20
|
|
429
442
|
>,
|
|
430
443
|
'deleteTransaction' : ActorMethod<[string], Result>,
|
|
431
444
|
'disconnectBot' : ActorMethod<[string, Principal], Result_1>,
|
|
432
445
|
'doesTransactionExist' : ActorMethod<[string], boolean>,
|
|
433
446
|
'getAllInviteCodes' : ActorMethod<[], Array<[string, string]>>,
|
|
434
447
|
'getAllTransactions' : ActorMethod<[], Array<Transaction>>,
|
|
435
|
-
'getCurrentPhase' : ActorMethod<[string],
|
|
448
|
+
'getCurrentPhase' : ActorMethod<[string], Result_19>,
|
|
436
449
|
'getCycles' : ActorMethod<[], bigint>,
|
|
437
450
|
'getDocumentStorageCanister' : ActorMethod<[], [] | [Principal]>,
|
|
438
451
|
'getExpiringSearches' : ActorMethod<[bigint], Array<Transaction>>,
|
|
439
452
|
'getFlowState' : ActorMethod<[string], [] | [string]>,
|
|
453
|
+
'getHmlrFetch' : ActorMethod<[string], Result_18>,
|
|
440
454
|
'getInviteCode' : ActorMethod<[string], Result>,
|
|
441
455
|
'getLandRegistryCanister' : ActorMethod<[], string>,
|
|
442
456
|
'getLandRegistryDetail' : ActorMethod<
|
|
@@ -488,7 +502,6 @@ export interface _SERVICE {
|
|
|
488
502
|
* / create. Counterpart to `removeParty` (which is seller/admin-driven and
|
|
489
503
|
* / gated against primary parties) — this is the "I want out" path for a
|
|
490
504
|
* / joined buyer or any other access-list member.
|
|
491
|
-
* /
|
|
492
505
|
* / Gates:
|
|
493
506
|
* / - Anonymous callers rejected outright.
|
|
494
507
|
* / - Seller / createdBy cannot leave their own transaction (they
|
|
@@ -496,7 +509,6 @@ export interface _SERVICE {
|
|
|
496
509
|
* / - Caller must currently have access to the transaction.
|
|
497
510
|
* / - Locked after contract exchange (same posture as assignBuyer):
|
|
498
511
|
* / once the deal is signed nobody walks away by toggling a button.
|
|
499
|
-
* /
|
|
500
512
|
* / Effects:
|
|
501
513
|
* / - Caller is removed from `accessList`.
|
|
502
514
|
* / - If the caller is the assigned buyer (`txn.buyer == msg.caller`),
|
|
@@ -506,7 +518,6 @@ export interface _SERVICE {
|
|
|
506
518
|
* / reusable by a new joiner.
|
|
507
519
|
* / - Audit event `buyer_left` (or `party_left` for non-buyer leavers)
|
|
508
520
|
* / is logged via the existing ledger pattern.
|
|
509
|
-
* /
|
|
510
521
|
* / Concurrency: uses the same acquireTxLock/finally releaseTxLock
|
|
511
522
|
* / posture as assignBuyer so a leave can't race against an in-flight
|
|
512
523
|
* / stage write.
|
|
@@ -525,7 +536,6 @@ export interface _SERVICE {
|
|
|
525
536
|
* / 2. Stale-pin txs where the org's pinned principal changed after the
|
|
526
537
|
* / tx was created (e.g. admin password reset → new ICP key, or org
|
|
527
538
|
* / ownership transferred to a different admin).
|
|
528
|
-
* /
|
|
529
539
|
* / Idempotent over the target: re-running with the same developerPrincipal
|
|
530
540
|
* / returns an error (already at target), so admin-driven backfill scripts
|
|
531
541
|
* / don't silently re-touch state. Always rejects:
|
|
@@ -551,6 +561,7 @@ export interface _SERVICE {
|
|
|
551
561
|
Result_1
|
|
552
562
|
>,
|
|
553
563
|
'recordFormUpload' : ActorMethod<[string, string, string, string], Result_1>,
|
|
564
|
+
'recordHmlrFetched' : ActorMethod<[string, string, string], Result_1>,
|
|
554
565
|
/**
|
|
555
566
|
* / Record a single party's signature on-chain.
|
|
556
567
|
* / The canister identifies the caller as buyer or seller from the transaction principals.
|
|
@@ -72,7 +72,11 @@ export const idlFactory = ({ IDL }) => {
|
|
|
72
72
|
'regulatoryBody' : RegulatoryBody,
|
|
73
73
|
});
|
|
74
74
|
const Result_1 = IDL.Variant({ 'ok' : IDL.Null, 'err' : IDL.Text });
|
|
75
|
-
const
|
|
75
|
+
const Result_21 = IDL.Variant({
|
|
76
|
+
'ok' : IDL.Record({ 'registrations' : IDL.Nat, 'transactions' : IDL.Nat }),
|
|
77
|
+
'err' : IDL.Text,
|
|
78
|
+
});
|
|
79
|
+
const Result_20 = IDL.Variant({
|
|
76
80
|
'ok' : IDL.Tuple(IDL.Text, IDL.Text),
|
|
77
81
|
'err' : IDL.Text,
|
|
78
82
|
});
|
|
@@ -193,7 +197,17 @@ export const idlFactory = ({ IDL }) => {
|
|
|
193
197
|
'preCompletion' : IDL.Null,
|
|
194
198
|
'searches' : IDL.Null,
|
|
195
199
|
});
|
|
196
|
-
const
|
|
200
|
+
const Result_19 = IDL.Variant({ 'ok' : Phase, 'err' : IDL.Text });
|
|
201
|
+
const HmlrFetchRecord = IDL.Record({
|
|
202
|
+
'fetchedAt' : IDL.Int,
|
|
203
|
+
'fetchedBy' : IDL.Principal,
|
|
204
|
+
'titleNumber' : IDL.Text,
|
|
205
|
+
'responseHash' : IDL.Text,
|
|
206
|
+
});
|
|
207
|
+
const Result_18 = IDL.Variant({
|
|
208
|
+
'ok' : IDL.Opt(HmlrFetchRecord),
|
|
209
|
+
'err' : IDL.Text,
|
|
210
|
+
});
|
|
197
211
|
const Notification = IDL.Record({
|
|
198
212
|
'id' : IDL.Nat,
|
|
199
213
|
'documentHash' : IDL.Text,
|
|
@@ -423,6 +437,7 @@ export const idlFactory = ({ IDL }) => {
|
|
|
423
437
|
[Result_1],
|
|
424
438
|
[],
|
|
425
439
|
),
|
|
440
|
+
'backfillTransactionMembers' : IDL.Func([], [Result_21], []),
|
|
426
441
|
'bootstrapDocumentStorageCanister' : IDL.Func(
|
|
427
442
|
[IDL.Principal],
|
|
428
443
|
[Result],
|
|
@@ -484,7 +499,7 @@ export const idlFactory = ({ IDL }) => {
|
|
|
484
499
|
IDL.Nat64,
|
|
485
500
|
IDL.Text,
|
|
486
501
|
],
|
|
487
|
-
[
|
|
502
|
+
[Result_20],
|
|
488
503
|
[],
|
|
489
504
|
),
|
|
490
505
|
'deleteTransaction' : IDL.Func([IDL.Text], [Result], []),
|
|
@@ -496,7 +511,7 @@ export const idlFactory = ({ IDL }) => {
|
|
|
496
511
|
['query'],
|
|
497
512
|
),
|
|
498
513
|
'getAllTransactions' : IDL.Func([], [IDL.Vec(Transaction)], ['query']),
|
|
499
|
-
'getCurrentPhase' : IDL.Func([IDL.Text], [
|
|
514
|
+
'getCurrentPhase' : IDL.Func([IDL.Text], [Result_19], ['query']),
|
|
500
515
|
'getCycles' : IDL.Func([], [IDL.Nat], ['query']),
|
|
501
516
|
'getDocumentStorageCanister' : IDL.Func(
|
|
502
517
|
[],
|
|
@@ -509,6 +524,7 @@ export const idlFactory = ({ IDL }) => {
|
|
|
509
524
|
['query'],
|
|
510
525
|
),
|
|
511
526
|
'getFlowState' : IDL.Func([IDL.Text], [IDL.Opt(IDL.Text)], ['query']),
|
|
527
|
+
'getHmlrFetch' : IDL.Func([IDL.Text], [Result_18], ['query']),
|
|
512
528
|
'getInviteCode' : IDL.Func([IDL.Text], [Result], ['query']),
|
|
513
529
|
'getLandRegistryCanister' : IDL.Func([], [IDL.Text], ['query']),
|
|
514
530
|
'getLandRegistryDetail' : IDL.Func(
|
|
@@ -613,6 +629,11 @@ export const idlFactory = ({ IDL }) => {
|
|
|
613
629
|
[Result_1],
|
|
614
630
|
[],
|
|
615
631
|
),
|
|
632
|
+
'recordHmlrFetched' : IDL.Func(
|
|
633
|
+
[IDL.Text, IDL.Text, IDL.Text],
|
|
634
|
+
[Result_1],
|
|
635
|
+
[],
|
|
636
|
+
),
|
|
616
637
|
'recordPartySignature' : IDL.Func(
|
|
617
638
|
[IDL.Text, IDL.Text, IDL.Text],
|
|
618
639
|
[Result],
|