commit 5bc7c2d2e65c21c2f87f9634e545c44ae5a5e36e
parent bbed91fb176f78fe41086cfb32b95a24a37e6396
Author: Florian Dold <dold@taler.net>
Date: Sun, 19 Jul 2026 13:53:24 +0200
db: store keys, hashes and signatures as BLOBs
Smaller on disk and in indexes. Record types still expose Crockford
strings, converted at the field mapping.
Diffstat:
5 files changed, 942 insertions(+), 483 deletions(-)
diff --git a/packages/taler-wallet-core/src/db-sqlite-schema.ts b/packages/taler-wallet-core/src/db-sqlite-schema.ts
@@ -42,7 +42,7 @@
*
* Bump this when adding a migration to {@link schemaMigrations}.
*/
-export const SQLITE_SCHEMA_VERSION = 1;
+export const SQLITE_SCHEMA_VERSION = 3;
/**
* A single, ordered schema evolution step.
@@ -74,6 +74,140 @@ export interface SchemaMigration {
* grows as dbtx-sqlite.ts does, rather than being written speculatively for
* all 43 tables at once.
*/
+
+/**
+ * Columns stored as BLOB whose record fields are Crockford base32 strings.
+ *
+ * Single source of truth: the mappers convert according to this, and a test
+ * asserts that a populated database matches it exactly. Both matter, because
+ * neither the type checker nor sqlite will complain if they diverge — sqlite
+ * happily stores a string in a BLOB-declared column, and a TEXT value never
+ * compares equal to a BLOB one, so a half-converted column returns no rows
+ * and raises no error.
+ *
+ * Phase 1 of SQLITE_BLOB_PLAN.md: the hot, high-row-count tables. Columns in
+ * other tables holding the same kind of value (for example
+ * refund_items.coin_pub) are still TEXT. That is safe because no query joins
+ * across them — they are separate tables compared only against bound
+ * parameters — but it is why the phases have to be finished rather than left
+ * half-done.
+ */
+export const BLOB_COLUMNS: Readonly<Record<string, readonly string[]>> = {
+ coin_availability: ["denom_pub_hash", "exchange_master_pub"],
+ coin_history: ["coin_pub"],
+ coins: [
+ "blinding_key",
+ "coin_ev_hash",
+ "coin_priv",
+ "coin_pub",
+ "denom_pub_hash",
+ ],
+ denomination_families: ["exchange_master_pub"],
+ denominations: ["denom_pub_hash", "exchange_master_pub", "master_sig"],
+ deposit_groups: [
+ "contract_terms_hash",
+ "merchant_priv",
+ "merchant_pub",
+ "nonce_priv",
+ "nonce_pub",
+ ],
+ donation_planchets: [
+ "bks",
+ "donation_unit_pub_hash",
+ "donor_tax_id_hash",
+ "udi_nonce",
+ ],
+ donation_receipts: [
+ "donation_unit_pub_hash",
+ "donor_tax_id_hash",
+ "udi_nonce",
+ ],
+ exchange_details: ["master_public_key"],
+ exchange_sign_keys: ["master_sig", "signkey_pub"],
+ exchanges: [
+ "current_account_priv",
+ "current_account_pub",
+ "details_pointer_master_pub",
+ ],
+ global_currency_auditors: ["auditor_pub"],
+ global_currency_exchanges: ["exchange_master_pub"],
+ peer_pull_credit: [
+ "contract_enc_nonce",
+ "contract_priv",
+ "contract_pub",
+ "contract_terms_hash",
+ "kyc_payto_hash",
+ "merge_priv",
+ "merge_pub",
+ "purse_priv",
+ "purse_pub",
+ ],
+ peer_pull_debit: ["contract_priv", "contract_terms_hash", "purse_pub"],
+ peer_push_credit: [
+ "contract_priv",
+ "contract_terms_hash",
+ "kyc_payto_hash",
+ "merge_priv",
+ "purse_pub",
+ ],
+ peer_push_debit: [
+ "contract_enc_nonce",
+ "contract_priv",
+ "contract_pub",
+ "contract_terms_hash",
+ "merge_priv",
+ "merge_pub",
+ "purse_priv",
+ "purse_pub",
+ ],
+ planchets: [
+ "blinding_key",
+ "coin_ev_hash",
+ "coin_priv",
+ "coin_pub",
+ "denom_pub_hash",
+ "withdraw_sig",
+ ],
+ purchases: [
+ "donau_tax_id_hash",
+ "merchant_pay_sig",
+ "nonce_priv",
+ "nonce_pub",
+ "secret_seed",
+ ],
+ refresh_sessions: ["session_public_seed"],
+ refund_items: ["coin_pub"],
+ reserves: ["reserve_priv", "reserve_pub"],
+ slates: [
+ "blinding_key",
+ "token_ev_hash",
+ "token_family_hash",
+ "token_issue_pub_hash",
+ "token_use_priv",
+ "token_use_pub",
+ ],
+ tokens: [
+ "blinding_key",
+ "token_ev_hash",
+ "token_family_hash",
+ "token_issue_pub_hash",
+ "token_use_priv",
+ "token_use_pub",
+ ],
+ withdrawal_groups: [
+ "contract_priv",
+ "kyc_payto_hash",
+ "reserve_priv",
+ "reserve_pub",
+ "secret_seed",
+ ],
+};
+
+/** True when the column is stored as a BLOB. */
+export function isBlobColumn(table: string, column: string): boolean {
+ return BLOB_COLUMNS[table]?.includes(column) ?? false;
+}
+
export const SQLITE_BASELINE_SCHEMA = `
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
@@ -135,8 +269,8 @@ CREATE TABLE IF NOT EXISTS operation_retries (
-- callers store it on the exchange entry, so ids must not be reused.
CREATE TABLE IF NOT EXISTS reserves (
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
- reserve_pub TEXT NOT NULL,
- reserve_priv TEXT NOT NULL,
+ reserve_pub BLOB NOT NULL,
+ reserve_priv BLOB NOT NULL,
status INTEGER,
requirement_row INTEGER,
threshold_requested TEXT,
@@ -154,9 +288,9 @@ CREATE INDEX IF NOT EXISTS reserves_by_reserve_pub
-- wrong lookup.
CREATE TABLE IF NOT EXISTS denominations (
exchange_base_url TEXT NOT NULL,
- denom_pub_hash TEXT NOT NULL,
+ denom_pub_hash BLOB NOT NULL,
denom_pub TEXT NOT NULL,
- exchange_master_pub TEXT NOT NULL,
+ exchange_master_pub BLOB NOT NULL,
currency TEXT NOT NULL,
value TEXT NOT NULL,
denomination_family_serial INTEGER,
@@ -171,7 +305,7 @@ CREATE TABLE IF NOT EXISTS denominations (
is_offered INTEGER NOT NULL,
is_revoked INTEGER NOT NULL,
is_lost INTEGER,
- master_sig TEXT NOT NULL,
+ master_sig BLOB NOT NULL,
verification_status INTEGER NOT NULL,
PRIMARY KEY (exchange_base_url, denom_pub_hash)
);
@@ -192,7 +326,7 @@ CREATE TABLE IF NOT EXISTS global_currency_exchanges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
currency TEXT NOT NULL,
exchange_base_url TEXT NOT NULL,
- exchange_master_pub TEXT NOT NULL
+ exchange_master_pub BLOB NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS global_currency_exchanges_by_cur_url_pub
ON global_currency_exchanges (currency, exchange_base_url,
@@ -202,7 +336,7 @@ CREATE TABLE IF NOT EXISTS global_currency_auditors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
currency TEXT NOT NULL,
auditor_base_url TEXT NOT NULL,
- auditor_pub TEXT NOT NULL
+ auditor_pub BLOB NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS global_currency_auditors_by_cur_url_pub
ON global_currency_auditors (currency, auditor_base_url, auditor_pub);
@@ -218,8 +352,8 @@ CREATE INDEX IF NOT EXISTS bank_accounts_by_payto_uri
ON bank_accounts (payto_uri);
CREATE TABLE IF NOT EXISTS tokens (
- token_use_pub TEXT PRIMARY KEY,
- token_use_priv TEXT NOT NULL,
+ token_use_pub BLOB PRIMARY KEY,
+ token_use_priv BLOB NOT NULL,
purchase_id TEXT NOT NULL,
transaction_id TEXT,
choice_index INTEGER,
@@ -227,15 +361,15 @@ CREATE TABLE IF NOT EXISTS tokens (
repeat_index INTEGER,
merchant_base_url TEXT NOT NULL,
kind TEXT NOT NULL,
- token_issue_pub_hash TEXT NOT NULL,
- token_family_hash TEXT,
+ token_issue_pub_hash BLOB NOT NULL,
+ token_family_hash BLOB,
valid_after INTEGER NOT NULL,
valid_before INTEGER NOT NULL,
token_issue_sig TEXT NOT NULL,
token_use_sig TEXT,
token_ev TEXT NOT NULL,
- token_ev_hash TEXT NOT NULL,
- blinding_key TEXT NOT NULL,
+ token_ev_hash BLOB NOT NULL,
+ blinding_key BLOB NOT NULL,
-- Inherited from TokenFamilyInfo.
slug TEXT NOT NULL,
name TEXT NOT NULL,
@@ -252,8 +386,8 @@ CREATE INDEX IF NOT EXISTS tokens_by_family_hash
ON tokens (token_family_hash);
CREATE TABLE IF NOT EXISTS slates (
- token_use_pub TEXT PRIMARY KEY,
- token_use_priv TEXT NOT NULL,
+ token_use_pub BLOB PRIMARY KEY,
+ token_use_priv BLOB NOT NULL,
purchase_id TEXT NOT NULL,
transaction_id TEXT,
choice_index INTEGER,
@@ -261,14 +395,14 @@ CREATE TABLE IF NOT EXISTS slates (
repeat_index INTEGER,
merchant_base_url TEXT NOT NULL,
kind TEXT NOT NULL,
- token_issue_pub_hash TEXT NOT NULL,
- token_family_hash TEXT,
+ token_issue_pub_hash BLOB NOT NULL,
+ token_family_hash BLOB,
valid_after INTEGER NOT NULL,
valid_before INTEGER NOT NULL,
token_use_sig TEXT,
token_ev TEXT NOT NULL,
- token_ev_hash TEXT NOT NULL,
- blinding_key TEXT NOT NULL,
+ token_ev_hash BLOB NOT NULL,
+ blinding_key BLOB NOT NULL,
-- Inherited from TokenFamilyInfo.
slug TEXT NOT NULL,
name TEXT NOT NULL,
@@ -287,7 +421,7 @@ CREATE INDEX IF NOT EXISTS slates_by_purchase_choice_output_repeat
CREATE TABLE IF NOT EXISTS refresh_sessions (
refresh_group_id TEXT NOT NULL,
coin_index INTEGER NOT NULL,
- session_public_seed TEXT,
+ session_public_seed BLOB,
amount_refresh_output TEXT NOT NULL,
new_denoms TEXT NOT NULL,
noreveal_index INTEGER,
@@ -321,31 +455,31 @@ CREATE TABLE IF NOT EXISTS donation_summaries (
);
CREATE TABLE IF NOT EXISTS donation_planchets (
- udi_nonce TEXT PRIMARY KEY,
+ udi_nonce BLOB PRIMARY KEY,
donau_base_url TEXT NOT NULL,
- donor_tax_id_hash TEXT NOT NULL,
+ donor_tax_id_hash BLOB NOT NULL,
donor_hash_salt TEXT NOT NULL,
donor_tax_id TEXT NOT NULL,
donation_year INTEGER NOT NULL,
proposal_id TEXT NOT NULL,
udi_index INTEGER NOT NULL,
blinded_udi TEXT NOT NULL,
- bks TEXT NOT NULL,
- donation_unit_pub_hash TEXT NOT NULL,
+ bks BLOB NOT NULL,
+ donation_unit_pub_hash BLOB NOT NULL,
value TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS donation_planchets_by_proposal
ON donation_planchets (proposal_id);
CREATE TABLE IF NOT EXISTS donation_receipts (
- udi_nonce TEXT PRIMARY KEY,
+ udi_nonce BLOB PRIMARY KEY,
status INTEGER NOT NULL,
donau_base_url TEXT NOT NULL,
proposal_id TEXT NOT NULL,
donation_year INTEGER NOT NULL,
- donation_unit_pub_hash TEXT NOT NULL,
+ donation_unit_pub_hash BLOB NOT NULL,
donation_unit_sig TEXT NOT NULL,
- donor_tax_id_hash TEXT NOT NULL,
+ donor_tax_id_hash BLOB NOT NULL,
donor_hash_salt TEXT NOT NULL,
donor_tax_id TEXT NOT NULL,
value TEXT NOT NULL,
@@ -367,10 +501,10 @@ CREATE TABLE IF NOT EXISTS purchases (
abort_refresh_group_id TEXT,
abort_reason TEXT,
fail_reason TEXT,
- nonce_priv TEXT NOT NULL,
- nonce_pub TEXT NOT NULL,
+ nonce_priv BLOB NOT NULL,
+ nonce_pub BLOB NOT NULL,
choice_index INTEGER,
- secret_seed TEXT,
+ secret_seed BLOB,
download TEXT,
-- The ONLY copy of download.fulfillmentUrl: stripped from the JSON on
-- write and re-inserted on read, so the indexed value cannot drift from
@@ -379,12 +513,12 @@ CREATE TABLE IF NOT EXISTS purchases (
pay_info TEXT,
pending_removed_coin_pubs TEXT,
timestamp_first_successful_pay INTEGER,
- merchant_pay_sig TEXT,
+ merchant_pay_sig BLOB,
pos_confirmation TEXT,
donau_output_index INTEGER,
donau_base_url TEXT,
donau_amount TEXT,
- donau_tax_id_hash TEXT,
+ donau_tax_id_hash BLOB,
donau_tax_id_salt TEXT,
donau_tax_id TEXT,
donau_year INTEGER,
@@ -425,12 +559,12 @@ CREATE TABLE IF NOT EXISTS deposit_groups (
currency TEXT NOT NULL,
amount TEXT NOT NULL,
wire_transfer_deadline INTEGER NOT NULL,
- merchant_pub TEXT NOT NULL,
- merchant_priv TEXT NOT NULL,
- nonce_priv TEXT NOT NULL,
- nonce_pub TEXT NOT NULL,
+ merchant_pub BLOB NOT NULL,
+ merchant_priv BLOB NOT NULL,
+ nonce_priv BLOB NOT NULL,
+ nonce_pub BLOB NOT NULL,
wire TEXT NOT NULL,
- contract_terms_hash TEXT NOT NULL,
+ contract_terms_hash BLOB NOT NULL,
pay_coin_selection TEXT,
pay_coin_selection_uid TEXT,
total_pay_cost TEXT NOT NULL,
@@ -489,19 +623,19 @@ CREATE INDEX IF NOT EXISTS denom_loss_events_by_status
ON denom_loss_events (status);
CREATE TABLE IF NOT EXISTS peer_push_debit (
- purse_pub TEXT PRIMARY KEY,
+ purse_pub BLOB PRIMARY KEY,
exchange_base_url TEXT NOT NULL,
restrict_scope TEXT,
amount TEXT NOT NULL,
total_cost TEXT NOT NULL,
coin_sel TEXT,
- contract_terms_hash TEXT NOT NULL,
- purse_priv TEXT NOT NULL,
- merge_pub TEXT NOT NULL,
- merge_priv TEXT NOT NULL,
- contract_priv TEXT NOT NULL,
- contract_pub TEXT NOT NULL,
- contract_enc_nonce TEXT NOT NULL,
+ contract_terms_hash BLOB NOT NULL,
+ purse_priv BLOB NOT NULL,
+ merge_pub BLOB NOT NULL,
+ merge_priv BLOB NOT NULL,
+ contract_priv BLOB NOT NULL,
+ contract_pub BLOB NOT NULL,
+ contract_enc_nonce BLOB NOT NULL,
purse_expiration INTEGER NOT NULL,
timestamp_created INTEGER NOT NULL,
abort_refresh_group_id TEXT,
@@ -515,18 +649,18 @@ CREATE INDEX IF NOT EXISTS peer_push_debit_by_status
CREATE TABLE IF NOT EXISTS peer_push_credit (
peer_push_credit_id TEXT PRIMARY KEY,
exchange_base_url TEXT NOT NULL,
- purse_pub TEXT NOT NULL,
- merge_priv TEXT NOT NULL,
- contract_priv TEXT NOT NULL,
+ purse_pub BLOB NOT NULL,
+ merge_priv BLOB NOT NULL,
+ contract_priv BLOB NOT NULL,
timestamp INTEGER NOT NULL,
estimated_amount_effective TEXT NOT NULL,
- contract_terms_hash TEXT NOT NULL,
+ contract_terms_hash BLOB NOT NULL,
status INTEGER NOT NULL,
abort_reason TEXT,
fail_reason TEXT,
withdrawal_group_id TEXT,
currency TEXT,
- kyc_payto_hash TEXT,
+ kyc_payto_hash BLOB,
kyc_access_token TEXT,
kyc_last_check_status INTEGER,
kyc_last_check_code INTEGER,
@@ -545,12 +679,12 @@ CREATE INDEX IF NOT EXISTS peer_push_credit_by_withdrawal_group
CREATE TABLE IF NOT EXISTS peer_pull_debit (
peer_pull_debit_id TEXT PRIMARY KEY,
- purse_pub TEXT NOT NULL,
+ purse_pub BLOB NOT NULL,
exchange_base_url TEXT NOT NULL,
amount TEXT NOT NULL,
- contract_terms_hash TEXT NOT NULL,
+ contract_terms_hash BLOB NOT NULL,
timestamp_created INTEGER NOT NULL,
- contract_priv TEXT NOT NULL,
+ contract_priv BLOB NOT NULL,
status INTEGER NOT NULL,
total_cost_estimated TEXT NOT NULL,
abort_refresh_group_id TEXT,
@@ -566,21 +700,21 @@ CREATE INDEX IF NOT EXISTS peer_pull_debit_by_exchange_and_contract_priv
ON peer_pull_debit (exchange_base_url, contract_priv);
CREATE TABLE IF NOT EXISTS peer_pull_credit (
- purse_pub TEXT PRIMARY KEY,
+ purse_pub BLOB PRIMARY KEY,
exchange_base_url TEXT NOT NULL,
amount TEXT NOT NULL,
estimated_amount_effective TEXT NOT NULL,
- purse_priv TEXT NOT NULL,
- contract_terms_hash TEXT NOT NULL,
- merge_pub TEXT NOT NULL,
- merge_priv TEXT NOT NULL,
- contract_pub TEXT NOT NULL,
- contract_priv TEXT NOT NULL,
- contract_enc_nonce TEXT NOT NULL,
+ purse_priv BLOB NOT NULL,
+ contract_terms_hash BLOB NOT NULL,
+ merge_pub BLOB NOT NULL,
+ merge_priv BLOB NOT NULL,
+ contract_pub BLOB NOT NULL,
+ contract_priv BLOB NOT NULL,
+ contract_enc_nonce BLOB NOT NULL,
merge_timestamp INTEGER NOT NULL,
merge_reserve_row_id INTEGER NOT NULL,
status INTEGER NOT NULL,
- kyc_payto_hash TEXT,
+ kyc_payto_hash BLOB,
kyc_access_token TEXT,
kyc_last_check_status INTEGER,
kyc_last_check_code INTEGER,
@@ -621,7 +755,7 @@ CREATE TABLE IF NOT EXISTS exchanges (
-- WalletExchangeDetailsPointer or undefined, i.e. a required key, so the
-- mapper always sets it and uses the master pub being NULL as the signal
-- that there is no pointer.
- details_pointer_master_pub TEXT,
+ details_pointer_master_pub BLOB,
details_pointer_currency TEXT,
details_pointer_update_clock INTEGER,
entry_status INTEGER NOT NULL,
@@ -636,8 +770,8 @@ CREATE TABLE IF NOT EXISTS exchanges (
last_keys_etag TEXT,
next_refresh_check_stamp INTEGER NOT NULL,
current_merge_reserve_row_id INTEGER,
- current_account_priv TEXT,
- current_account_pub TEXT,
+ current_account_priv BLOB,
+ current_account_pub BLOB,
peer_payments_disabled INTEGER,
direct_deposit_disabled INTEGER,
no_fees INTEGER
@@ -646,7 +780,7 @@ CREATE TABLE IF NOT EXISTS exchanges (
CREATE TABLE IF NOT EXISTS exchange_details (
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
exchange_base_url TEXT NOT NULL,
- master_public_key TEXT NOT NULL,
+ master_public_key BLOB NOT NULL,
currency TEXT NOT NULL,
auditors TEXT NOT NULL,
protocol_version_range TEXT NOT NULL,
@@ -673,11 +807,11 @@ CREATE TABLE IF NOT EXISTS exchange_sign_keys (
REFERENCES exchange_details(row_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED,
- signkey_pub TEXT NOT NULL,
+ signkey_pub BLOB NOT NULL,
stamp_start INTEGER NOT NULL,
stamp_expire INTEGER NOT NULL,
stamp_end INTEGER NOT NULL,
- master_sig TEXT NOT NULL,
+ master_sig BLOB NOT NULL,
PRIMARY KEY (exchange_details_row_id, signkey_pub)
);
@@ -686,7 +820,7 @@ CREATE TABLE IF NOT EXISTS exchange_sign_keys (
CREATE TABLE IF NOT EXISTS denomination_families (
denomination_family_serial INTEGER PRIMARY KEY AUTOINCREMENT,
exchange_base_url TEXT NOT NULL,
- exchange_master_pub TEXT NOT NULL,
+ exchange_master_pub BLOB NOT NULL,
value TEXT NOT NULL,
fee_withdraw TEXT NOT NULL,
fee_deposit TEXT NOT NULL,
@@ -727,11 +861,11 @@ CREATE TABLE IF NOT EXISTS withdrawal_groups (
-- would let the indexed column and the payload disagree.
taler_withdraw_uri TEXT,
-- Promoted so the whole PeerPullCredit variant needs no JSON at all.
- contract_priv TEXT,
+ contract_priv BLOB,
bank_info TEXT,
exchange_credit_accounts TEXT,
is_foreign_account INTEGER,
- kyc_payto_hash TEXT,
+ kyc_payto_hash BLOB,
kyc_access_token TEXT,
kyc_last_check_status INTEGER,
kyc_last_check_code INTEGER,
@@ -739,9 +873,9 @@ CREATE TABLE IF NOT EXISTS withdrawal_groups (
kyc_last_aml_review INTEGER,
kyc_last_deny INTEGER,
kyc_withdrawal_delay TEXT,
- secret_seed TEXT NOT NULL,
- reserve_pub TEXT NOT NULL,
- reserve_priv TEXT NOT NULL,
+ secret_seed BLOB NOT NULL,
+ reserve_pub BLOB NOT NULL,
+ reserve_priv BLOB NOT NULL,
exchange_base_url TEXT,
timestamp_start INTEGER NOT NULL,
timestamp_finish INTEGER,
@@ -766,17 +900,17 @@ CREATE INDEX IF NOT EXISTS withdrawal_groups_by_taler_withdraw_uri
ON withdrawal_groups (taler_withdraw_uri);
CREATE TABLE IF NOT EXISTS planchets (
- coin_pub TEXT PRIMARY KEY,
- coin_priv TEXT NOT NULL,
+ coin_pub BLOB PRIMARY KEY,
+ coin_priv BLOB NOT NULL,
withdrawal_group_id TEXT NOT NULL,
coin_idx INTEGER NOT NULL,
planchet_status INTEGER NOT NULL,
last_error TEXT,
- denom_pub_hash TEXT NOT NULL,
- blinding_key TEXT NOT NULL,
- withdraw_sig TEXT NOT NULL,
+ denom_pub_hash BLOB NOT NULL,
+ blinding_key BLOB NOT NULL,
+ withdraw_sig BLOB NOT NULL,
coin_ev TEXT NOT NULL,
- coin_ev_hash TEXT NOT NULL,
+ coin_ev_hash BLOB NOT NULL,
age_commitment_proof TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS planchets_by_group_and_index
@@ -787,13 +921,13 @@ CREATE INDEX IF NOT EXISTS planchets_by_coin_ev
ON planchets (coin_ev_hash);
CREATE TABLE IF NOT EXISTS coins (
- coin_pub TEXT PRIMARY KEY,
- coin_priv TEXT NOT NULL,
+ coin_pub BLOB PRIMARY KEY,
+ coin_priv BLOB NOT NULL,
exchange_base_url TEXT NOT NULL,
- denom_pub_hash TEXT NOT NULL,
+ denom_pub_hash BLOB NOT NULL,
denom_sig TEXT NOT NULL,
- blinding_key TEXT NOT NULL,
- coin_ev_hash TEXT NOT NULL,
+ blinding_key BLOB NOT NULL,
+ coin_ev_hash BLOB NOT NULL,
-- TEXT, not INTEGER: CoinStatus is a string enum ("fresh", "denom-loss",
-- ...), unlike every other status column in this schema.
status TEXT NOT NULL,
@@ -820,11 +954,11 @@ CREATE INDEX IF NOT EXISTS coins_by_exchange_denom_age_status
CREATE TABLE IF NOT EXISTS coin_availability (
exchange_base_url TEXT NOT NULL,
- denom_pub_hash TEXT NOT NULL,
+ denom_pub_hash BLOB NOT NULL,
max_age INTEGER NOT NULL,
currency TEXT NOT NULL,
value TEXT NOT NULL,
- exchange_master_pub TEXT,
+ exchange_master_pub BLOB,
fresh_coin_count INTEGER NOT NULL,
visible_coin_count INTEGER NOT NULL,
pending_refresh_output_count INTEGER,
@@ -838,7 +972,7 @@ CREATE INDEX IF NOT EXISTS coin_availability_by_exchange_age_fresh
ON coin_availability (exchange_base_url, max_age, fresh_coin_count);
CREATE TABLE IF NOT EXISTS coin_history (
- coin_pub TEXT PRIMARY KEY,
+ coin_pub BLOB PRIMARY KEY,
history TEXT NOT NULL
);
@@ -871,7 +1005,7 @@ CREATE TABLE IF NOT EXISTS refund_items (
execution_time INTEGER NOT NULL,
obtained_time INTEGER NOT NULL,
refund_amount TEXT NOT NULL,
- coin_pub TEXT NOT NULL,
+ coin_pub BLOB NOT NULL,
rtxid INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS refund_items_by_group
diff --git a/packages/taler-wallet-core/src/dbtx-bench.ts b/packages/taler-wallet-core/src/dbtx-bench.ts
@@ -35,6 +35,7 @@ import {
AmountString,
CoinStatus,
DenomKeyType,
+ encodeCrock,
Logger,
} from "@gnu-taler/taler-util";
import {
@@ -93,16 +94,16 @@ function median(xs: number[]): number {
}
/**
- * Deterministic pseudo-Crockford value of the right length.
+ * Deterministic Crockford value of the right length.
*
- * Fixed length matters: the point of the benchmark is partly to compare
- * against a schema that stores these as BLOBs, so the strings have to be the
- * size the real ones are (52 characters for a 32-byte key, 103 for a 64-byte
- * hash) or the comparison measures the wrong thing.
+ * Built by encoding bytes rather than by picking characters. Real wallet
+ * values are always canonical encodings, and picking characters produces
+ * non-canonical ones: 52 Crockford characters carry 260 bits while a key is
+ * 256, so only about one random string in 16 is a spelling the database will
+ * hand back. Generating those measured a workload the wallet never runs.
*/
-function crockLike(seed: string, len: number): string {
- const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
- let out = "";
+function crockLike(seed: string, numBytes: number): string {
+ const bytes = new Uint8Array(numBytes);
// Math.imul, not `*`: 32-bit hash multiplication overflows a double and
// silently loses the low bits, which collapses distinct seeds onto the same
// output. An earlier version did that and produced 4 distinct coin public
@@ -112,16 +113,16 @@ function crockLike(seed: string, len: number): string {
for (let i = 0; i < seed.length; i++) {
h = Math.imul(h ^ seed.charCodeAt(i), 16777619);
}
- for (let i = 0; i < len; i++) {
+ for (let i = 0; i < numBytes; i++) {
h = Math.imul(h ^ (h >>> 15), 2246822519);
h = (h ^ (h >>> 13)) >>> 0;
- out += alphabet[h % 32];
+ bytes[i] = h & 0xff;
}
- return out;
+ return encodeCrock(bytes);
}
-const key = (s: string) => crockLike(s, 52);
-const hash = (s: string) => crockLike(s, 103);
+const key = (s: string) => crockLike(s, 32);
+const hash = (s: string) => crockLike(s, 64);
function exchangeUrl(i: number): string {
return `https://exchange-${i}.test/`;
diff --git a/packages/taler-wallet-core/src/dbtx-conformance-cases.ts b/packages/taler-wallet-core/src/dbtx-conformance-cases.ts
@@ -26,6 +26,8 @@
import {
AmountString,
CoinStatus,
+ decodeCrock,
+ encodeCrock,
MerchantContractTokenKind,
RefreshReason,
DenomKeyType,
@@ -91,6 +93,62 @@ import {
} from "./db-common.js";
import { ConformanceCase } from "./dbtx-conformance.js";
+/**
+ * Deterministic Crockford base32 value derived from a readable label.
+ *
+ * The columns holding keys, hashes and signatures are BLOBs in the native
+ * schema, so the DAL decodes them on write and re-encodes on read: a fixture
+ * value like "dph-a" is not decodable, and would throw. Labels stay readable
+ * at the call sites and this maps them, so a record written as ck("coin-1")
+ * is found by a query for ck("coin-1").
+ *
+ * Built by encoding bytes rather than by picking characters, because not
+ * every 52-character Crockford string is a canonical encoding of 32 bytes:
+ * 52 characters carry 260 bits and a key is 256, so the spare bits do not
+ * survive a decode/encode round trip. Generating characters directly
+ * produced values that came back differing in the last character.
+ */
+function ckBytes(label: string, numBytes: number): Uint8Array {
+ const out = new Uint8Array(numBytes);
+ let h = 2166136261;
+ for (let i = 0; i < label.length; i++) {
+ h = Math.imul(h ^ label.charCodeAt(i), 16777619);
+ }
+ for (let i = 0; i < numBytes; i++) {
+ h = Math.imul(h ^ (h >>> 15), 2246822519);
+ h = (h ^ (h >>> 13)) >>> 0;
+ out[i] = h & 0xff;
+ }
+ return out;
+}
+
+/**
+ * Idempotent: a value that is already a canonical encoding of the right size
+ * is returned unchanged.
+ *
+ * These labels flow through fixtures, query arguments and assertions, and all
+ * three have to agree. Idempotence means a site can be wrapped without
+ * checking whether it was wrapped already, which is what makes converting
+ * them mechanically safe.
+ */
+function ckOfSize(label: string, numBytes: number): string {
+ try {
+ const decoded = decodeCrock(label);
+ if (decoded.length === numBytes && encodeCrock(decoded) === label) {
+ return label;
+ }
+ } catch (e) {
+ // Not Crockford at all; fall through and derive one.
+ }
+ return encodeCrock(ckBytes(label, numBytes));
+}
+
+/** A key-sized (32-byte) Crockford value. */
+const ck = (label: string): string => ckOfSize(label, 32);
+
+/** A hash-sized (64-byte) Crockford value. */
+const ckh = (label: string): string => ckOfSize(label, 64);
+
const ts = (seconds: number): DbProtocolTimestamp =>
timestampProtocolToDb(TalerProtocolTimestamp.fromSeconds(seconds));
@@ -115,13 +173,13 @@ function makeDenomination(
// second implementation tried to persist it.
const denom: WalletDenomination = {
exchangeBaseUrl,
- denomPubHash,
+ denomPubHash: ckh(denomPubHash),
denomPub: {
cipher: DenomKeyType.Rsa,
rsa_public_key: "dummy",
age_mask: 0,
},
- exchangeMasterPub: "master-pub",
+ exchangeMasterPub: ck("master-pub"),
currency: "TESTKUDOS",
value: amt("TESTKUDOS:1"),
denominationFamilySerial: opts.familySerial ?? 1,
@@ -138,7 +196,7 @@ function makeDenomination(
isOffered: opts.isOffered ?? true,
isRevoked: false,
isLost: false,
- masterSig: "master-sig",
+ masterSig: ckh("master-sig"),
verificationStatus: DenominationVerificationStatus.VerifiedGood,
};
return denom;
@@ -146,16 +204,16 @@ function makeDenomination(
function makeCoin(coinPub: string): WalletCoin {
const coin: WalletCoin = {
- coinPub,
- coinPriv: `priv-${coinPub}`,
+ coinPub: ck(coinPub),
+ coinPriv: ck(`priv-${coinPub}`),
exchangeBaseUrl: "https://exchange.test/",
- denomPubHash: "dph-default",
+ denomPubHash: ckh("dph-default"),
denomSig: {
cipher: DenomKeyType.Rsa,
rsa_signature: "sig-blob",
},
- blindingKey: "bk-1",
- coinEvHash: `evh-${coinPub}`,
+ blindingKey: ck("bk-1"),
+ coinEvHash: ckh(`evh-${coinPub}`),
status: CoinStatus.Fresh,
maxAge: 0,
ageCommitmentProof: undefined,
@@ -163,7 +221,7 @@ function makeCoin(coinPub: string): WalletCoin {
type: CoinSourceType.Withdraw,
withdrawalGroupId: "wg-default",
coinIndex: 0,
- reservePub: "rp-default",
+ reservePub: ck("rp-default"),
},
};
return coin;
@@ -176,7 +234,7 @@ function makeAvail(
): WalletCoinAvailability {
const rec: WalletCoinAvailability = {
exchangeBaseUrl,
- denomPubHash,
+ denomPubHash: ckh(denomPubHash),
maxAge,
currency: "TESTKUDOS",
value: amt("TESTKUDOS:1"),
@@ -209,7 +267,7 @@ function makeExchangeDetails(
): WalletExchangeDetails {
const det: WalletExchangeDetails = {
exchangeBaseUrl,
- masterPublicKey,
+ masterPublicKey: ck(masterPublicKey),
currency: "TESTKUDOS",
auditors: [],
protocolVersionRange: "18:0:1",
@@ -232,11 +290,11 @@ function makeSignKey(
): WalletExchangeSignkeys {
const k: WalletExchangeSignkeys = {
exchangeDetailsRowId,
- signkeyPub,
+ signkeyPub: ck(signkeyPub),
stampStart: ts(100),
stampExpire: ts(200),
stampEnd: ts(300),
- masterSig: "sig-1",
+ masterSig: ckh("sig-1"),
};
return k;
}
@@ -247,7 +305,7 @@ function makeFamilyParams(
): WalletDenomFamilyParams {
const p: WalletDenomFamilyParams = {
exchangeBaseUrl,
- exchangeMasterPub: "mpk-fam",
+ exchangeMasterPub: ck("mpk-fam"),
value: amt(value),
feeWithdraw: amt("TESTKUDOS:0.01"),
feeDeposit: amt("TESTKUDOS:0.01"),
@@ -273,9 +331,9 @@ function makeWithdrawalGroup(withdrawalGroupId: string): WalletWithdrawalGroup {
const wg: WalletWithdrawalGroup = {
withdrawalGroupId,
wgInfo: { withdrawalType: WithdrawalRecordType.BankManual },
- secretSeed: `seed-${withdrawalGroupId}`,
- reservePub: `rpub-${withdrawalGroupId}`,
- reservePriv: `rpriv-${withdrawalGroupId}`,
+ secretSeed: ck(`seed-${withdrawalGroupId}`),
+ reservePub: ck(`rpub-${withdrawalGroupId}`),
+ reservePriv: ck(`rpriv-${withdrawalGroupId}`),
timestampStart: tsPrecise(1000),
status: WithdrawalGroupStatus.PendingRegisteringBank,
};
@@ -288,20 +346,20 @@ function makePlanchet(
coinIdx: number,
): WalletPlanchet {
const pl: WalletPlanchet = {
- coinPub,
- coinPriv: `priv-${coinPub}`,
+ coinPub: ck(coinPub),
+ coinPriv: ck(`priv-${coinPub}`),
withdrawalGroupId,
coinIdx,
planchetStatus: PlanchetStatus.Pending,
lastError: undefined,
- denomPubHash: "dph-pl",
- blindingKey: "bk-pl",
- withdrawSig: "sig-pl",
+ denomPubHash: ckh("dph-pl"),
+ blindingKey: ck("bk-pl"),
+ withdrawSig: ckh("sig-pl"),
coinEv: {
cipher: DenomKeyType.Rsa,
rsa_blinded_planchet: "blinded",
},
- coinEvHash: `evh-${coinPub}`,
+ coinEvHash: ckh(`evh-${coinPub}`),
};
return pl;
}
@@ -310,7 +368,7 @@ function makeDownloadInfo(
fulfillmentUrl: string | undefined,
): WalletProposalDownloadInfo {
const dl: WalletProposalDownloadInfo = {
- contractTermsHash: "cth-1",
+ contractTermsHash: ckh("cth-1"),
currency: "TESTKUDOS",
contractTermsMerchantSig: "sig-1",
...(fulfillmentUrl !== undefined ? { fulfillmentUrl } : undefined),
@@ -327,8 +385,8 @@ function makePurchase(proposalId: string): WalletPurchase {
downloadSessionId: undefined,
repurchaseProposalId: undefined,
purchaseStatus: PurchaseStatus.PendingDownloadingProposal,
- noncePriv: `npriv-${proposalId}`,
- noncePub: `npub-${proposalId}`,
+ noncePriv: ck(`npriv-${proposalId}`),
+ noncePub: ck(`npub-${proposalId}`),
secretSeed: undefined,
download: undefined,
payInfo: undefined,
@@ -393,18 +451,18 @@ const tokenFamilyFields = () => ({
function makeToken(tokenUsePub: string): WalletToken {
const tok: WalletToken = {
...tokenFamilyFields(),
- tokenUsePub,
- tokenUsePriv: `priv-${tokenUsePub}`,
+ tokenUsePub: ck(tokenUsePub),
+ tokenUsePriv: ck(`priv-${tokenUsePub}`),
purchaseId: "pur-tok",
merchantBaseUrl: "https://merchant.test/",
kind: MerchantContractTokenKind.Subscription,
- tokenIssuePubHash: "tiph-1",
+ tokenIssuePubHash: ckh("tiph-1"),
validAfter: ts(100),
validBefore: ts(200),
tokenIssueSig: { cipher: DenomKeyType.Rsa, rsa_signature: "isig" },
tokenEv: { cipher: DenomKeyType.Rsa, rsa_blinded_planchet: "blinded" },
- tokenEvHash: `evh-${tokenUsePub}`,
- blindingKey: "bk-tok",
+ tokenEvHash: ckh(`evh-${tokenUsePub}`),
+ blindingKey: ck("bk-tok"),
};
return tok;
}
@@ -418,20 +476,20 @@ function makeSlate(
): WalletSlate {
const sl: WalletSlate = {
...tokenFamilyFields(),
- tokenUsePub,
- tokenUsePriv: `priv-${tokenUsePub}`,
+ tokenUsePub: ck(tokenUsePub),
+ tokenUsePriv: ck(`priv-${tokenUsePub}`),
purchaseId,
choiceIndex,
outputIndex,
repeatIndex,
merchantBaseUrl: "https://merchant.test/",
kind: MerchantContractTokenKind.Subscription,
- tokenIssuePubHash: "tiph-1",
+ tokenIssuePubHash: ckh("tiph-1"),
validAfter: ts(100),
validBefore: ts(200),
tokenEv: { cipher: DenomKeyType.Rsa, rsa_blinded_planchet: "blinded" },
- tokenEvHash: `evh-${tokenUsePub}`,
- blindingKey: "bk-slate",
+ tokenEvHash: ckh(`evh-${tokenUsePub}`),
+ blindingKey: ck("bk-slate"),
};
return sl;
}
@@ -442,12 +500,12 @@ function makeDepositGroup(depositGroupId: string): WalletDepositGroup {
currency: "TESTKUDOS",
amount: amt("TESTKUDOS:5"),
wireTransferDeadline: ts(9999),
- merchantPub: "mpub",
- merchantPriv: "mpriv",
- noncePriv: "npriv",
- noncePub: "npub",
+ merchantPub: ck("mpub"),
+ merchantPriv: ck("mpriv"),
+ noncePriv: ck("npriv"),
+ noncePub: ck("npub"),
wire: { payto_uri: "payto://iban/DE1", salt: "salt-1" },
- contractTermsHash: "cth",
+ contractTermsHash: ckh("cth"),
totalPayCost: amt("TESTKUDOS:5.1"),
counterpartyEffectiveDepositAmount: amt("TESTKUDOS:5"),
timestampCreated: tsPrecise(1000),
@@ -483,7 +541,7 @@ function makeRefreshSession(
refreshGroupId,
coinIndex,
amountRefreshOutput: amt("TESTKUDOS:1"),
- newDenoms: [{ denomPubHash: "dph-1", count: 2 }],
+ newDenoms: [{ denomPubHash: ckh("dph-1"), count: 2 }],
};
return rs;
}
@@ -507,17 +565,17 @@ function makeRecoupGroup(
function makePeerPushDebit(pursePub: string): WalletPeerPushDebit {
const rec: WalletPeerPushDebit = {
- pursePub,
+ pursePub: ck(pursePub),
exchangeBaseUrl: "https://exchange.test/",
amount: amt("TESTKUDOS:3"),
totalCost: amt("TESTKUDOS:3.1"),
- contractTermsHash: "cth",
- pursePriv: "ppriv",
- mergePub: "mpub",
- mergePriv: "mpriv",
- contractPriv: "cpriv",
- contractPub: "cpub",
- contractEncNonce: "nonce",
+ contractTermsHash: ckh("cth"),
+ pursePriv: ck("ppriv"),
+ mergePub: ck("mpub"),
+ mergePriv: ck("mpriv"),
+ contractPriv: ck("cpriv"),
+ contractPub: ck("cpub"),
+ contractEncNonce: ck("nonce"),
purseExpiration: ts(9999),
timestampCreated: tsPrecise(1000),
status: PeerPushDebitStatus.PendingCreatePurse,
@@ -529,12 +587,12 @@ function makePeerPushCredit(peerPushCreditId: string): WalletPeerPushCredit {
const rec: WalletPeerPushCredit = {
peerPushCreditId,
exchangeBaseUrl: "https://exchange.test/",
- pursePub: `purse-${peerPushCreditId}`,
- mergePriv: "mpriv",
- contractPriv: "cpriv",
+ pursePub: ck(`purse-${peerPushCreditId}`),
+ mergePriv: ck("mpriv"),
+ contractPriv: ck("cpriv"),
timestamp: tsPrecise(1000),
estimatedAmountEffective: amt("TESTKUDOS:2"),
- contractTermsHash: "cth",
+ contractTermsHash: ckh("cth"),
status: PeerPushCreditStatus.PendingMerge,
withdrawalGroupId: undefined,
currency: undefined,
@@ -545,12 +603,12 @@ function makePeerPushCredit(peerPushCreditId: string): WalletPeerPushCredit {
function makePeerPullDebit(peerPullDebitId: string): WalletPeerPullDebit {
const rec: WalletPeerPullDebit = {
peerPullDebitId,
- pursePub: `purse-${peerPullDebitId}`,
+ pursePub: ck(`purse-${peerPullDebitId}`),
exchangeBaseUrl: "https://exchange.test/",
amount: amt("TESTKUDOS:4"),
- contractTermsHash: "cth",
+ contractTermsHash: ckh("cth"),
timestampCreated: tsPrecise(1000),
- contractPriv: "cpriv",
+ contractPriv: ck("cpriv"),
status: PeerPullDebitRecordStatus.PendingDeposit,
totalCostEstimated: amt("TESTKUDOS:4.1"),
};
@@ -559,17 +617,17 @@ function makePeerPullDebit(peerPullDebitId: string): WalletPeerPullDebit {
function makePeerPullCredit(pursePub: string): WalletPeerPullCredit {
const rec: WalletPeerPullCredit = {
- pursePub,
+ pursePub: ck(pursePub),
exchangeBaseUrl: "https://exchange.test/",
amount: amt("TESTKUDOS:6"),
estimatedAmountEffective: amt("TESTKUDOS:6"),
- pursePriv: "ppriv",
- contractTermsHash: "cth",
- mergePub: "mpub",
- mergePriv: "mpriv",
- contractPub: "cpub",
- contractPriv: "cpriv",
- contractEncNonce: "nonce",
+ pursePriv: ck("ppriv"),
+ contractTermsHash: ckh("cth"),
+ mergePub: ck("mpub"),
+ mergePriv: ck("mpriv"),
+ contractPub: ck("cpub"),
+ contractPriv: ck("cpriv"),
+ contractEncNonce: ck("nonce"),
mergeTimestamp: tsPrecise(1000),
mergeReserveRowId: 1,
status: PeerPullPaymentCreditStatus.PendingCreatePurse,
@@ -580,8 +638,8 @@ function makePeerPullCredit(pursePub: string): WalletPeerPullCredit {
function makeReserve(reservePub: string): WalletReserve {
const r: WalletReserve = {
- reservePub,
- reservePriv: `priv-of-${reservePub}`,
+ reservePub: ck(reservePub),
+ reservePriv: ck(`priv-of-${reservePub}`),
};
return r;
}
@@ -612,7 +670,7 @@ function makeRefundItem(
executionTime: ts(5000),
obtainedTime: tsPrecise(5000),
refundAmount: amt("TESTKUDOS:1"),
- coinPub,
+ coinPub: ck(coinPub),
rtxid,
};
}
@@ -640,7 +698,9 @@ export const conformanceCases: ConformanceCase[] = [
{
name: "get on a missing key returns undefined, not a throw",
async run(t, runner) {
- const got = await runner.runTx((tx) => tx.getCoin("no-such-coin-pub"));
+ const got = await runner.runTx((tx) =>
+ tx.getCoin(ck("no-such-coin-pub")),
+ );
t.equal(got, undefined);
},
},
@@ -671,8 +731,8 @@ export const conformanceCases: ConformanceCase[] = [
await tx.upsertDenomination(makeDenomination("https://e2/", "dph-a"));
});
const [d1, d2] = await runner.runTx(async (tx) => [
- await tx.getDenomination("https://e1/", "dph-a"),
- await tx.getDenomination("https://e2/", "dph-a"),
+ await tx.getDenomination("https://e1/", ckh("dph-a")),
+ await tx.getDenomination("https://e2/", ckh("dph-a")),
]);
t.ok(d1, "same hash under a different exchange must be a distinct row");
t.ok(d2);
@@ -721,12 +781,12 @@ export const conformanceCases: ConformanceCase[] = [
await tx.upsertRefundGroup(makeRefundGroup("grp-3"));
});
const got = await runner.runTx((tx) =>
- tx.getRefundItemByCoinAndRtxid("coin-9", 42),
+ tx.getRefundItemByCoinAndRtxid(ck("coin-9"), 42),
);
t.ok(got, "compound index lookup must find the item");
t.equal(got?.refundGroupId, "grp-3");
const miss = await runner.runTx((tx) =>
- tx.getRefundItemByCoinAndRtxid("coin-9", 43),
+ tx.getRefundItemByCoinAndRtxid(ck("coin-9"), 43),
);
t.equal(miss, undefined, "wrong rtxid must not match");
},
@@ -742,16 +802,26 @@ export const conformanceCases: ConformanceCase[] = [
// exchange entry silently.
async run(t, runner) {
const id1 = await runner.runTx((tx) =>
- tx.upsertReserve({ reservePub: "rp-1", reservePriv: "rv-1" } as any),
+ tx.upsertReserve({
+ reservePub: ck("rp-1"),
+ reservePriv: ck("rv-1"),
+ } as any),
);
const id2 = await runner.runTx((tx) =>
- tx.upsertReserve({ reservePub: "rp-2", reservePriv: "rv-2" } as any),
+ tx.upsertReserve({
+ reservePub: ck("rp-2"),
+ reservePriv: ck("rv-2"),
+ } as any),
);
t.equal(typeof id1, "number");
t.equal(typeof id2, "number");
t.ok(id1 !== id2, "generated ids must be distinct");
const back = await runner.runTx((tx) => tx.getReserve(id1));
- t.equal(back?.reservePub, "rp-1", "the returned id must address the row");
+ t.equal(
+ back?.reservePub,
+ ck("rp-1"),
+ "the returned id must address the row",
+ );
},
},
@@ -789,7 +859,7 @@ export const conformanceCases: ConformanceCase[] = [
);
t.equal(
found?.denomPubHash,
- "d-3",
+ ckh("d-3"),
"must return the first match in order",
);
},
@@ -884,7 +954,11 @@ export const conformanceCases: ConformanceCase[] = [
const found = await runner.runTx((tx) =>
tx.findDenominationByFamilyFromExpiry(111, ts(10000), () => true),
);
- t.equal(found?.denomPubHash, "new", "must skip records before the bound");
+ t.equal(
+ found?.denomPubHash,
+ ckh("new"),
+ "must skip records before the bound",
+ );
},
},
@@ -977,7 +1051,7 @@ export const conformanceCases: ConformanceCase[] = [
async run(t, runner) {
await runner.runTx(async (tx) => {
await tx.deletePurchase("no-such-proposal");
- await tx.deleteDenomination("https://nope/", "no-such-hash");
+ await tx.deleteDenomination("https://nope/", ckh("no-such-hash"));
await tx.deleteRefundGroup("no-such-group");
});
t.ok(true, "deleting a missing row must not throw");
@@ -1035,9 +1109,9 @@ export const conformanceCases: ConformanceCase[] = [
t.ok(typeof id1 === "number", "upsertReserve must return the row id");
t.ok(id1 !== id2, "row ids must be distinct");
const got = await runner.runTx((tx) =>
- tx.getReserveByReservePub("rpub-2"),
+ tx.getReserveByReservePub(ck("rpub-2")),
);
- t.equal(got?.reservePub, "rpub-2");
+ t.equal(got?.reservePub, ck("rpub-2"));
t.equal(got?.rowId, id2, "the id returned must be the id stored");
},
},
@@ -1164,14 +1238,14 @@ export const conformanceCases: ConformanceCase[] = [
tx.getRefundItemsByGroup("rg-del"),
);
t.equal(items.length, 2);
- const victim = items.find((i) => i.coinPub === "coin-d1");
+ const victim = items.find((i) => i.coinPub === ck("coin-d1"));
t.ok(victim?.id !== undefined, "stored items must carry their row id");
await runner.runTx((tx) => tx.deleteRefundItem(victim!.id!));
const left = await runner.runTx((tx) =>
tx.getRefundItemsByGroup("rg-del"),
);
t.equal(left.length, 1);
- t.equal(left[0].coinPub, "coin-d2");
+ t.equal(left[0].coinPub, ck("coin-d2"));
},
},
@@ -1192,7 +1266,7 @@ export const conformanceCases: ConformanceCase[] = [
),
);
t.equal(unverified.length, 1, "must filter on the status");
- t.equal(unverified[0].denomPubHash, "vs-a");
+ t.equal(unverified[0].denomPubHash, ckh("vs-a"));
},
},
// ---------------------------------------------------------------- coins
@@ -1202,7 +1276,7 @@ export const conformanceCases: ConformanceCase[] = [
async run(t, runner) {
const coin = makeCoin("cp-1");
await runner.runTx((tx) => tx.upsertCoin(coin));
- const got = await runner.runTx((tx) => tx.getCoin("cp-1"));
+ const got = await runner.runTx((tx) => tx.getCoin(ck("cp-1")));
t.ok(got, "coin should exist");
t.deepEqual(got, coin, "every field must survive the round trip");
t.ok(
@@ -1220,10 +1294,10 @@ export const conformanceCases: ConformanceCase[] = [
type: CoinSourceType.Withdraw,
withdrawalGroupId: "wg-1",
coinIndex: 3,
- reservePub: "rp-1",
+ reservePub: ck("rp-1"),
};
await runner.runTx((tx) => tx.upsertCoin(coin));
- const got = await runner.runTx((tx) => tx.getCoin("cp-src"));
+ const got = await runner.runTx((tx) => tx.getCoin(ck("cp-src")));
t.deepEqual(got?.coinSource, coin.coinSource);
t.deepEqual(got?.denomSig, coin.denomSig);
},
@@ -1235,7 +1309,7 @@ export const conformanceCases: ConformanceCase[] = [
const coin = makeCoin("cp-status");
coin.status = CoinStatus.Dormant;
await runner.runTx((tx) => tx.upsertCoin(coin));
- const got = await runner.runTx((tx) => tx.getCoin("cp-status"));
+ const got = await runner.runTx((tx) => tx.getCoin(ck("cp-status")));
t.equal(got?.status, CoinStatus.Dormant);
t.equal(typeof got?.status, "string", "CoinStatus must not be coerced");
},
@@ -1250,7 +1324,7 @@ export const conformanceCases: ConformanceCase[] = [
coin.visible = 1;
await runner.runTx((tx) => tx.upsertCoin(coin));
const all = await runner.runTx((tx) => tx.listAllCoins());
- const mine = all.filter((c) => c.coinPub === "cp-up");
+ const mine = all.filter((c) => c.coinPub === ck("cp-up"));
t.equal(mine.length, 1, "a second upsert must replace, not append");
t.equal(mine[0].status, CoinStatus.Dormant);
t.equal(mine[0].visible, 1);
@@ -1263,15 +1337,15 @@ export const conformanceCases: ConformanceCase[] = [
await runner.runTx(async (tx) => {
const a = makeCoin("cq-1");
a.exchangeBaseUrl = "https://ex-a/";
- a.denomPubHash = "dq-1";
+ a.denomPubHash = ckh("dq-1");
a.sourceTransactionId = "txn-1";
const b = makeCoin("cq-2");
b.exchangeBaseUrl = "https://ex-a/";
- b.denomPubHash = "dq-2";
+ b.denomPubHash = ckh("dq-2");
b.sourceTransactionId = "txn-2";
const c = makeCoin("cq-3");
c.exchangeBaseUrl = "https://ex-b/";
- c.denomPubHash = "dq-1";
+ c.denomPubHash = ckh("dq-1");
await tx.upsertCoin(a);
await tx.upsertCoin(b);
await tx.upsertCoin(c);
@@ -1285,14 +1359,14 @@ export const conformanceCases: ConformanceCase[] = [
);
t.equal(count, 2, "count must agree with the list");
const byDenom = await runner.runTx((tx) =>
- tx.getCoinsByDenomPubHash("dq-1"),
+ tx.getCoinsByDenomPubHash(ckh("dq-1")),
);
t.equal(byDenom.length, 2, "denom hash spans exchanges");
const bySrc = await runner.runTx((tx) =>
tx.getCoinsBySourceTransaction("txn-1"),
);
t.equal(bySrc.length, 1);
- t.equal(bySrc[0].coinPub, "cq-1");
+ t.equal(bySrc[0].coinPub, ck("cq-1"));
},
},
@@ -1304,11 +1378,11 @@ export const conformanceCases: ConformanceCase[] = [
await tx.upsertCoin(makeCoin("cb-2"));
});
const got = await runner.runTx((tx) =>
- tx.getCoinsByPubs(["cb-2", "cb-missing", "cb-1"]),
+ tx.getCoinsByPubs([ck("cb-2"), ck("cb-missing"), ck("cb-1")]),
);
t.deepEqual(
got.map((c) => c.coinPub),
- ["cb-2", "cb-1"],
+ [ck("cb-2"), ck("cb-1")],
"missing pubs are dropped, not returned as holes",
);
},
@@ -1321,18 +1395,18 @@ export const conformanceCases: ConformanceCase[] = [
for (let i = 0; i < 4; i++) {
const c = makeCoin(`cf-${i}`);
c.exchangeBaseUrl = "https://ex-f/";
- c.denomPubHash = "df-1";
+ c.denomPubHash = ckh("df-1");
c.maxAge = 21;
c.status = i === 3 ? CoinStatus.Dormant : CoinStatus.Fresh;
await tx.upsertCoin(c);
}
});
const all = await runner.runTx((tx) =>
- tx.getFreshCoinsByDenomAndAge("https://ex-f/", "df-1", 21, 100),
+ tx.getFreshCoinsByDenomAndAge("https://ex-f/", ckh("df-1"), 21, 100),
);
t.equal(all.length, 3, "the dormant coin must be excluded");
const limited = await runner.runTx((tx) =>
- tx.getFreshCoinsByDenomAndAge("https://ex-f/", "df-1", 21, 2),
+ tx.getFreshCoinsByDenomAndAge("https://ex-f/", ckh("df-1"), 21, 2),
);
t.equal(limited.length, 2, "the limit must be applied");
},
@@ -1344,16 +1418,19 @@ export const conformanceCases: ConformanceCase[] = [
await runner.runTx(async (tx) => {
await tx.upsertCoin(makeCoin("cd-1"));
await tx.upsertCoinHistory({
- coinPub: "cd-1",
+ coinPub: ck("cd-1"),
history: [{ type: "withdraw", transactionId: txnId("txn:cd-1") }],
});
});
- await runner.runTx((tx) => tx.deleteCoin("cd-1"));
- t.equal(await runner.runTx((tx) => tx.getCoin("cd-1")), undefined);
- const hist = await runner.runTx((tx) => tx.getCoinHistory("cd-1"));
+ await runner.runTx((tx) => tx.deleteCoin(ck("cd-1")));
+ t.equal(await runner.runTx((tx) => tx.getCoin(ck("cd-1"))), undefined);
+ const hist = await runner.runTx((tx) => tx.getCoinHistory(ck("cd-1")));
t.ok(hist, "deleteCoin must not delete the history record");
- await runner.runTx((tx) => tx.deleteCoinHistory("cd-1"));
- t.equal(await runner.runTx((tx) => tx.getCoinHistory("cd-1")), undefined);
+ await runner.runTx((tx) => tx.deleteCoinHistory(ck("cd-1")));
+ t.equal(
+ await runner.runTx((tx) => tx.getCoinHistory(ck("cd-1"))),
+ undefined,
+ );
},
},
@@ -1374,9 +1451,9 @@ export const conformanceCases: ConformanceCase[] = [
},
];
await runner.runTx((tx) =>
- tx.upsertCoinHistory({ coinPub: "ch-1", history }),
+ tx.upsertCoinHistory({ coinPub: ck("ch-1"), history }),
);
- const got = await runner.runTx((tx) => tx.getCoinHistory("ch-1"));
+ const got = await runner.runTx((tx) => tx.getCoinHistory(ck("ch-1")));
t.deepEqual(got?.history, history);
},
},
@@ -1391,10 +1468,10 @@ export const conformanceCases: ConformanceCase[] = [
await tx.upsertCoinAvailability(makeAvail("https://ea/", "da", 21));
});
const a = await runner.runTx((tx) =>
- tx.getCoinAvailability("https://ea/", "da", 0),
+ tx.getCoinAvailability("https://ea/", ckh("da"), 0),
);
const b = await runner.runTx((tx) =>
- tx.getCoinAvailability("https://ea/", "da", 21),
+ tx.getCoinAvailability("https://ea/", ckh("da"), 21),
);
t.ok(a && b, "differing maxAge must be distinct rows");
t.equal(a?.maxAge, 0);
@@ -1412,7 +1489,7 @@ export const conformanceCases: ConformanceCase[] = [
rec.pendingRefreshOutputCount = 2;
await runner.runTx((tx) => tx.upsertCoinAvailability(rec));
const got = await runner.runTx((tx) =>
- tx.getCoinAvailability("https://eu/", "du", 0),
+ tx.getCoinAvailability("https://eu/", ckh("du"), 0),
);
t.equal(got?.freshCoinCount, 9);
t.equal(got?.visibleCoinCount, 4);
@@ -1451,7 +1528,7 @@ export const conformanceCases: ConformanceCase[] = [
const hashes = got.map((a) => a.denomPubHash).sort();
t.deepEqual(
hashes,
- ["d-hi", "d-lf"],
+ [ckh("d-hi"), ckh("d-lf")].sort(),
"excludes only the zero-fresh row at the lower bound",
);
},
@@ -1465,7 +1542,7 @@ export const conformanceCases: ConformanceCase[] = [
await tx.upsertCoinAvailability(makeAvail("https://ed/", "dd", 21));
});
await runner.runTx((tx) =>
- tx.deleteCoinAvailability("https://ed/", "dd", 0),
+ tx.deleteCoinAvailability("https://ed/", ckh("dd"), 0),
);
const left = await runner.runTx((tx) =>
tx.getCoinAvailabilityByExchange("https://ed/"),
@@ -1481,7 +1558,7 @@ export const conformanceCases: ConformanceCase[] = [
async run(t, runner) {
const ex = makeExchange("https://ex-rt/");
ex.detailsPointer = {
- masterPublicKey: "mpk-1",
+ masterPublicKey: ck("mpk-1"),
currency: "TESTKUDOS",
updateClock: tsPrecise(4242),
};
@@ -1556,7 +1633,11 @@ export const conformanceCases: ConformanceCase[] = [
const rowId = await runner.runTx((tx) => tx.upsertExchangeDetails(det));
t.ok(typeof rowId === "number", "must return the generated row id");
const got = await runner.runTx((tx) =>
- tx.getExchangeDetailsByPointer("https://ed-1/", "TESTKUDOS", "mpk-a"),
+ tx.getExchangeDetailsByPointer(
+ "https://ed-1/",
+ "TESTKUDOS",
+ ck("mpk-a"),
+ ),
);
t.equal(got?.rowId, rowId);
t.deepEqual(got?.wireInfo, det.wireInfo, "nested JSON must survive");
@@ -1590,7 +1671,7 @@ export const conformanceCases: ConformanceCase[] = [
await tx.upsertExchangeDetails(det);
const ex = makeExchange("https://ep/");
ex.detailsPointer = {
- masterPublicKey: "mpk-p",
+ masterPublicKey: ck("mpk-p"),
currency: "TESTKUDOS",
updateClock: tsPrecise(1),
};
@@ -1599,7 +1680,7 @@ export const conformanceCases: ConformanceCase[] = [
const got = await runner.runTx((tx) =>
tx.getExchangeDetails("https://ep/"),
);
- t.equal(got?.masterPublicKey, "mpk-p");
+ t.equal(got?.masterPublicKey, ck("mpk-p"));
},
},
@@ -1647,13 +1728,16 @@ export const conformanceCases: ConformanceCase[] = [
tx.getExchangeSignKeysByDetailsRowId(rowId),
);
t.equal(keys.length, 2);
- t.deepEqual(keys.map((k) => k.signkeyPub).sort(), ["sk-1", "sk-2"]);
- await runner.runTx((tx) => tx.deleteExchangeSignKey(rowId, "sk-1"));
+ t.deepEqual(
+ keys.map((k) => k.signkeyPub).sort(),
+ [ck("sk-1"), ck("sk-2")].sort(),
+ );
+ await runner.runTx((tx) => tx.deleteExchangeSignKey(rowId, ck("sk-1")));
const left = await runner.runTx((tx) =>
tx.getExchangeSignKeysByDetailsRowId(rowId),
);
t.equal(left.length, 1);
- t.equal(left[0].signkeyPub, "sk-2");
+ t.equal(left[0].signkeyPub, ck("sk-2"));
},
},
@@ -1665,13 +1749,13 @@ export const conformanceCases: ConformanceCase[] = [
);
const key = makeSignKey(rowId, "sk-dup");
await runner.runTx((tx) => tx.upsertExchangeSignKey(key));
- key.masterSig = "sig-updated";
+ key.masterSig = ckh("sig-updated");
await runner.runTx((tx) => tx.upsertExchangeSignKey(key));
const keys = await runner.runTx((tx) =>
tx.getExchangeSignKeysByDetailsRowId(rowId),
);
t.equal(keys.length, 1, "must replace, not append");
- t.equal(keys[0].masterSig, "sig-updated");
+ t.equal(keys[0].masterSig, ckh("sig-updated"));
},
},
@@ -1904,7 +1988,7 @@ export const conformanceCases: ConformanceCase[] = [
},
{
withdrawalType: WithdrawalRecordType.PeerPullCredit,
- contractPriv: "cpriv-1",
+ contractPriv: ck("cpriv-1"),
},
{ withdrawalType: WithdrawalRecordType.PeerPushCredit },
{ withdrawalType: WithdrawalRecordType.Recoup },
@@ -2022,8 +2106,8 @@ export const conformanceCases: ConformanceCase[] = [
const byIdx = await runner.runTx((tx) =>
tx.getPlanchetByGroupAndIndex("wg-p", 1),
);
- t.equal(byIdx?.coinPub, "pl-2");
- const direct = await runner.runTx((tx) => tx.getPlanchet("pl-1"));
+ t.equal(byIdx?.coinPub, ck("pl-2"));
+ const direct = await runner.runTx((tx) => tx.getPlanchet(ck("pl-1")));
t.deepEqual(direct, makePlanchet("pl-1", "wg-p", 0));
},
},
@@ -2034,7 +2118,7 @@ export const conformanceCases: ConformanceCase[] = [
const pl = makePlanchet("pl-err", "wg-e", 0);
pl.lastError = undefined;
await runner.runTx((tx) => tx.upsertPlanchet(pl));
- const got = await runner.runTx((tx) => tx.getPlanchet("pl-err"));
+ const got = await runner.runTx((tx) => tx.getPlanchet(ck("pl-err")));
t.ok(
got !== undefined && "lastError" in got,
"lastError is declared as a required key",
@@ -2067,7 +2151,7 @@ export const conformanceCases: ConformanceCase[] = [
0,
);
t.ok(
- await runner.runTx((tx) => tx.getPlanchet("pl-keep")),
+ await runner.runTx((tx) => tx.getPlanchet(ck("pl-keep"))),
"another group must be untouched",
);
},
@@ -2518,21 +2602,21 @@ export const conformanceCases: ConformanceCase[] = [
async run(t, runner) {
const rec = makePeerPushDebit("ppd-1");
await runner.runTx((tx) => tx.upsertPeerPushDebit(rec));
- const got = await runner.runTx((tx) => tx.getPeerPushDebit("ppd-1"));
+ const got = await runner.runTx((tx) => tx.getPeerPushDebit(ck("ppd-1")));
t.deepEqual(withoutUndefined(got), withoutUndefined(rec));
const done = makePeerPushDebit("ppd-2");
done.status = PeerPushDebitStatus.Done;
await runner.runTx((tx) => tx.upsertPeerPushDebit(done));
const active = await runner.runTx((tx) => tx.getActivePeerPushDebits());
- t.ok(active.map((r) => r.pursePub).includes("ppd-1"));
- t.ok(!active.map((r) => r.pursePub).includes("ppd-2"));
+ t.ok(active.map((r) => r.pursePub).includes(ck("ppd-1")));
+ t.ok(!active.map((r) => r.pursePub).includes(ck("ppd-2")));
t.equal(
(await runner.runTx((tx) => tx.listAllPeerPushDebits())).length,
2,
);
- await runner.runTx((tx) => tx.deletePeerPushDebit("ppd-1"));
+ await runner.runTx((tx) => tx.deletePeerPushDebit(ck("ppd-1")));
t.equal(
- await runner.runTx((tx) => tx.getPeerPushDebit("ppd-1")),
+ await runner.runTx((tx) => tx.getPeerPushDebit(ck("ppd-1"))),
undefined,
);
},
@@ -2542,21 +2626,21 @@ export const conformanceCases: ConformanceCase[] = [
name: "peer push credit: round trip, contract-priv lookup, active range",
async run(t, runner) {
const rec = makePeerPushCredit("ppc-1");
- rec.contractPriv = "cpriv-find-me";
+ rec.contractPriv = ck("cpriv-find-me");
await runner.runTx((tx) => tx.upsertPeerPushCredit(rec));
const got = await runner.runTx((tx) => tx.getPeerPushCredit("ppc-1"));
t.deepEqual(withoutUndefined(got), withoutUndefined(rec));
const byPriv = await runner.runTx((tx) =>
tx.getPeerPushCreditByExchangeAndContractPriv(
rec.exchangeBaseUrl,
- "cpriv-find-me",
+ ck("cpriv-find-me"),
),
);
t.equal(byPriv?.peerPushCreditId, "ppc-1");
const wrongExchange = await runner.runTx((tx) =>
tx.getPeerPushCreditByExchangeAndContractPriv(
"https://other/",
- "cpriv-find-me",
+ ck("cpriv-find-me"),
),
);
t.equal(wrongExchange, undefined, "both components must match");
@@ -2578,14 +2662,14 @@ export const conformanceCases: ConformanceCase[] = [
name: "peer pull debit: round trip, contract-priv lookup, active range",
async run(t, runner) {
const rec = makePeerPullDebit("ppld-1");
- rec.contractPriv = "cpriv-pull";
+ rec.contractPriv = ck("cpriv-pull");
await runner.runTx((tx) => tx.upsertPeerPullDebit(rec));
const got = await runner.runTx((tx) => tx.getPeerPullDebit("ppld-1"));
t.deepEqual(withoutUndefined(got), withoutUndefined(rec));
const byPriv = await runner.runTx((tx) =>
tx.getPeerPullDebitByExchangeAndContractPriv(
rec.exchangeBaseUrl,
- "cpriv-pull",
+ ck("cpriv-pull"),
),
);
t.equal(byPriv?.peerPullDebitId, "ppld-1");
@@ -2612,21 +2696,23 @@ export const conformanceCases: ConformanceCase[] = [
async run(t, runner) {
const rec = makePeerPullCredit("pplc-1");
await runner.runTx((tx) => tx.upsertPeerPullCredit(rec));
- const got = await runner.runTx((tx) => tx.getPeerPullCredit("pplc-1"));
+ const got = await runner.runTx((tx) =>
+ tx.getPeerPullCredit(ck("pplc-1")),
+ );
t.deepEqual(withoutUndefined(got), withoutUndefined(rec));
const done = makePeerPullCredit("pplc-2");
done.status = PeerPullPaymentCreditStatus.Done;
await runner.runTx((tx) => tx.upsertPeerPullCredit(done));
const active = await runner.runTx((tx) => tx.getActivePeerPullCredits());
- t.ok(active.map((r) => r.pursePub).includes("pplc-1"));
- t.ok(!active.map((r) => r.pursePub).includes("pplc-2"));
+ t.ok(active.map((r) => r.pursePub).includes(ck("pplc-1")));
+ t.ok(!active.map((r) => r.pursePub).includes(ck("pplc-2")));
t.equal(
(await runner.runTx((tx) => tx.listAllPeerPullCredits())).length,
2,
);
- await runner.runTx((tx) => tx.deletePeerPullCredit("pplc-1"));
+ await runner.runTx((tx) => tx.deletePeerPullCredit(ck("pplc-1")));
t.equal(
- await runner.runTx((tx) => tx.getPeerPullCredit("pplc-1")),
+ await runner.runTx((tx) => tx.getPeerPullCredit(ck("pplc-1"))),
undefined,
);
},
@@ -2642,7 +2728,7 @@ export const conformanceCases: ConformanceCase[] = [
// its own body once cost five silently dropped columns here.
const tok = makeToken("tk-1");
await runner.runTx((tx) => tx.upsertToken(tok));
- const got = await runner.runTx((tx) => tx.getToken("tk-1"));
+ const got = await runner.runTx((tx) => tx.getToken(ck("tk-1")));
t.deepEqual(withoutUndefined(got), withoutUndefined(tok));
t.equal(got?.slug, tok.slug, "inherited fields must persist");
t.deepEqual(got?.tokenIssuePub, tok.tokenIssuePub);
@@ -2657,25 +2743,28 @@ export const conformanceCases: ConformanceCase[] = [
async run(t, runner) {
await runner.runTx(async (tx) => {
const a = makeToken("tk-a");
- a.tokenIssuePubHash = "tiph-shared";
+ a.tokenIssuePubHash = ckh("tiph-shared");
const b = makeToken("tk-b");
- b.tokenIssuePubHash = "tiph-shared";
+ b.tokenIssuePubHash = ckh("tiph-shared");
const c = makeToken("tk-c");
- c.tokenIssuePubHash = "tiph-other";
+ c.tokenIssuePubHash = ckh("tiph-other");
await tx.upsertToken(a);
await tx.upsertToken(b);
await tx.upsertToken(c);
});
const byHash = await runner.runTx((tx) =>
- tx.getTokensByIssuePubHash("tiph-shared"),
+ tx.getTokensByIssuePubHash(ckh("tiph-shared")),
);
t.equal(byHash.length, 2);
t.equal((await runner.runTx((tx) => tx.listTokens())).length, 3);
- await runner.runTx((tx) => tx.deleteToken("tk-a"));
- t.equal(await runner.runTx((tx) => tx.getToken("tk-a")), undefined);
+ await runner.runTx((tx) => tx.deleteToken(ck("tk-a")));
+ t.equal(await runner.runTx((tx) => tx.getToken(ck("tk-a"))), undefined);
t.equal(
- (await runner.runTx((tx) => tx.getTokensByIssuePubHash("tiph-shared")))
- .length,
+ (
+ await runner.runTx((tx) =>
+ tx.getTokensByIssuePubHash(ckh("tiph-shared")),
+ )
+ ).length,
1,
);
},
@@ -2693,14 +2782,14 @@ export const conformanceCases: ConformanceCase[] = [
const one = await runner.runTx((tx) => tx.getSlate("pur-1", 0, 0, 1));
t.equal(
one?.tokenUsePub,
- "sl-2",
+ ck("sl-2"),
"all four components must select the slate",
);
const byChoice = await runner.runTx((tx) =>
tx.getSlatesByPurchaseAndChoice("pur-1", 0),
);
t.equal(byChoice.length, 3, "choice 1 must not be included");
- await runner.runTx((tx) => tx.deleteSlate("sl-2"));
+ await runner.runTx((tx) => tx.deleteSlate(ck("sl-2")));
t.equal(
await runner.runTx((tx) => tx.getSlate("pur-1", 0, 0, 1)),
undefined,
@@ -2732,4 +2821,28 @@ export const conformanceCases: ConformanceCase[] = [
t.deepEqual(gotSigned?.tokenUseSig, signed.tokenUseSig);
},
},
+ {
+ name: "coin availability: exchange master pub round trips",
+ async run(t, runner) {
+ // Exercises a column that no other case populates; the sqlite
+ // storage-class test can only verify columns that have rows.
+ const rec = makeAvail("https://emp/", "d-emp", 0);
+ rec.exchangeMasterPub = ck("master-emp");
+ await runner.runTx((tx) => tx.upsertCoinAvailability(rec));
+ const got = await runner.runTx((tx) =>
+ tx.getCoinAvailability("https://emp/", ckh("d-emp"), 0),
+ );
+ t.equal(got?.exchangeMasterPub, ck("master-emp"));
+ const unset = makeAvail("https://emp2/", "d-emp2", 0);
+ await runner.runTx((tx) => tx.upsertCoinAvailability(unset));
+ const got2 = await runner.runTx((tx) =>
+ tx.getCoinAvailability("https://emp2/", ckh("d-emp2"), 0),
+ );
+ t.equal(
+ got2?.exchangeMasterPub,
+ undefined,
+ "an unset optional key must stay unset",
+ );
+ },
+ },
];
diff --git a/packages/taler-wallet-core/src/dbtx-sqlite.test.ts b/packages/taler-wallet-core/src/dbtx-sqlite.test.ts
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Tests specific to the native sqlite backend, as opposed to the
+ * backend-neutral conformance suite.
+ *
+ * The storage-class check here is the one test that catches a write path
+ * which stored TEXT into a BLOB column. Nothing else can: sqlite accepts a
+ * string in a BLOB-declared column, the value reads back fine through the
+ * same code that wrote it, and only a *lookup* against a correctly-encoded
+ * parameter fails — silently, by matching nothing.
+ */
+
+import { createNodeHelperSqlite3Impl } from "@gnu-taler/idb-bridge/node-helper-sqlite3-impl";
+import assert from "node:assert";
+import { test } from "node:test";
+
+import { BLOB_COLUMNS } from "./db-sqlite-schema.js";
+import { conformanceCases } from "./dbtx-conformance-cases.js";
+import { makeSqliteRunner } from "./dbtx-runners.js";
+
+/**
+ * Run every conformance case against one database, then inspect how the
+ * values actually landed.
+ *
+ * Reusing the conformance cases as the workload means this covers whatever
+ * write paths the suite covers, rather than a hand-written sample that would
+ * drift away from it.
+ */
+test("sqlite: BLOB columns really hold blobs", async (t) => {
+ const impl = await createNodeHelperSqlite3Impl({ enableTracing: false });
+ const db = await impl.open(":memory:");
+ const runner = await makeSqliteRunner(":memory:");
+
+ // Populate through the DAL, using the same cases the conformance suite
+ // runs, so every write path they exercise is represented here.
+ const asserts = {
+ equal: () => {},
+ deepEqual: () => {},
+ ok: () => {},
+ fail: () => {
+ throw Error("unreachable");
+ },
+ };
+ for (const c of conformanceCases) {
+ try {
+ await c.run(asserts as any, runner);
+ } catch (e) {
+ // A case that fails its own assertions is the conformance suite's
+ // problem, not this test's. What matters here is what got written.
+ }
+ }
+
+ const offenders: string[] = [];
+ const missing: string[] = [];
+
+ for (const [table, columns] of Object.entries(BLOB_COLUMNS)) {
+ for (const column of columns) {
+ const rows = await runner.runTx(async (tx: any) => {
+ // Reach past the DAL deliberately: the point is to see the storage
+ // class, which the DAL exists to hide.
+ return await (tx as any).all(
+ `SELECT typeof("${column}") AS t, COUNT(*) AS n FROM "${table}"` +
+ ` WHERE "${column}" IS NOT NULL GROUP BY typeof("${column}")`,
+ );
+ });
+ if (rows.length === 0) {
+ missing.push(`${table}.${column}`);
+ continue;
+ }
+ for (const r of rows) {
+ if (r.t !== "blob") {
+ offenders.push(`${table}.${column} has ${r.n} row(s) of ${r.t}`);
+ }
+ }
+ }
+ }
+
+ await runner.close();
+ await db.close();
+
+ assert.deepStrictEqual(
+ offenders,
+ [],
+ `columns declared BLOB that hold something else:\n${offenders.join("\n")}`,
+ );
+
+ // Not a failure — a column with no rows simply was not exercised — but
+ // worth surfacing, because an unexercised column is an unverified one.
+ if (missing.length > 0) {
+ t.diagnostic(`BLOB columns with no rows to check: ${missing.join(", ")}`);
+ }
+});
diff --git a/packages/taler-wallet-core/src/dbtx-sqlite.ts b/packages/taler-wallet-core/src/dbtx-sqlite.ts
@@ -33,6 +33,8 @@ import {
import {
AmountString,
CoinStatus,
+ decodeCrock,
+ encodeCrock,
MerchantContractTokenKind,
stringifyScopeInfo,
DenomLossEventType,
@@ -304,6 +306,51 @@ function optStr(v: Sqlite3Value | undefined): string | undefined {
return v == null ? undefined : (v as string);
}
+/**
+ * Encode a Crockford base32 record field for a BLOB column.
+ *
+ * Applied at the individual field mapping, never folded into a shared row
+ * builder: when a record field eventually becomes Uint8Array end to end, that
+ * one line loses its call and nothing else moves. See SQLITE_BLOB_PLAN.md.
+ */
+function crockToDb(v: string): Uint8Array {
+ return decodeCrock(v);
+}
+
+function optCrockToDb(v: string | undefined | null): Uint8Array | null {
+ return v == null ? null : decodeCrock(v);
+}
+
+/**
+ * Decode a BLOB column back into the Crockford string the record exposes.
+ *
+ * Throws rather than coercing if the column came back as TEXT: that means a
+ * write path stored a string into a BLOB column, which otherwise stays
+ * invisible until a lookup silently matches nothing.
+ */
+function dbToCrock(v: Sqlite3Value | undefined): string {
+ if (!(v instanceof Uint8Array)) {
+ throw Error(
+ `expected a BLOB column, got ${typeof v}; a write path is storing` +
+ ` TEXT into a BLOB column`,
+ );
+ }
+ return encodeCrock(v);
+}
+
+function dbToOptCrock(v: Sqlite3Value | undefined): string | undefined {
+ return v == null ? undefined : dbToCrock(v);
+}
+
+/** Stable map key for a BLOB value. */
+function blobKey(v: Uint8Array): string {
+ let out = "";
+ for (let i = 0; i < v.length; i++) {
+ out += v[i].toString(16).padStart(2, "0");
+ }
+ return out;
+}
+
function jsonToDb(v: unknown): string {
return JSON.stringify(v);
}
@@ -476,8 +523,8 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async upsertReserve(rec: WalletReserve): Promise<number> {
const cols = {
- pub: rec.reservePub,
- priv: rec.reservePriv,
+ pub: crockToDb(rec.reservePub),
+ priv: crockToDb(rec.reservePriv),
status: rec.status ?? null,
requirement_row: rec.requirementRow ?? null,
threshold_requested: rec.thresholdRequested ?? null,
@@ -531,7 +578,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletReserve | undefined> {
const row = await this.first(
"SELECT * FROM reserves" + " WHERE reserve_pub = $pub",
- { pub: reservePub },
+ { pub: crockToDb(reservePub) },
);
return row ? this.rowToReserve(row) : undefined;
}
@@ -539,8 +586,8 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToReserve(row: ResultRow): WalletReserve {
return {
rowId: num(row.row_id),
- reservePub: str(row.reserve_pub),
- reservePriv: str(row.reserve_priv),
+ reservePub: dbToCrock(row.reserve_pub),
+ reservePriv: dbToCrock(row.reserve_priv),
...(row.status != null ? { status: num(row.status) } : undefined),
...(row.requirement_row != null
? { requirementRow: num(row.requirement_row) }
@@ -605,9 +652,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
verification_status = excluded.verification_status`,
{
exchange_base_url: rec.exchangeBaseUrl,
- denom_pub_hash: rec.denomPubHash,
+ denom_pub_hash: crockToDb(rec.denomPubHash),
denom_pub: jsonToDb(rec.denomPub),
- exchange_master_pub: rec.exchangeMasterPub ?? null,
+ exchange_master_pub: crockToDb(rec.exchangeMasterPub),
currency: rec.currency,
value: rec.value,
family_serial: rec.denominationFamilySerial ?? null,
@@ -622,7 +669,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
is_offered: boolToDb(rec.isOffered),
is_revoked: boolToDb(rec.isRevoked),
is_lost: boolToDb(rec.isLost),
- master_sig: rec.masterSig,
+ master_sig: crockToDb(rec.masterSig),
verification_status: rec.verificationStatus,
},
);
@@ -635,7 +682,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
const row = await this.first(
"SELECT * FROM denominations" +
" WHERE exchange_base_url = $url AND denom_pub_hash = $hash",
- { url: exchangeBaseUrl, hash: denomPubHash },
+ { url: exchangeBaseUrl, hash: crockToDb(denomPubHash) },
);
return row ? this.rowToDenomination(row) : undefined;
}
@@ -667,7 +714,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
await this.run(
"DELETE FROM denominations" +
" WHERE exchange_base_url = $url AND denom_pub_hash = $hash",
- { url: exchangeBaseUrl, hash: denomPubHash },
+ { url: exchangeBaseUrl, hash: crockToDb(denomPubHash) },
);
}
@@ -691,7 +738,11 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletDenomination | undefined> {
const batchSize = 1;
let afterExpiry: number = minStampExpireWithdraw;
- let afterHash: string | undefined = undefined;
+ // The cursor stays in the column's own representation (a BLOB now) and
+ // is fed straight back into the next query. Decoding it to a string and
+ // re-encoding would be pointless work, and binding the string form would
+ // silently match nothing.
+ let afterHash: Uint8Array | undefined = undefined;
while (true) {
const rows: ResultRow[] =
afterHash === undefined
@@ -731,16 +782,20 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
}
const last = rows[rows.length - 1];
afterExpiry = num(last.stamp_expire_withdraw);
- afterHash = str(last.denom_pub_hash);
+ const lastHash = last.denom_pub_hash;
+ if (!(lastHash instanceof Uint8Array)) {
+ throw Error("denominations.denom_pub_hash must be a BLOB column");
+ }
+ afterHash = lastHash;
}
}
private rowToDenomination(row: ResultRow): WalletDenomination {
const denom: WalletDenomination = {
exchangeBaseUrl: str(row.exchange_base_url),
- denomPubHash: str(row.denom_pub_hash),
+ denomPubHash: dbToCrock(row.denom_pub_hash),
denomPub: dbToJson(row.denom_pub),
- exchangeMasterPub: str(row.exchange_master_pub),
+ exchangeMasterPub: dbToCrock(row.exchange_master_pub),
currency: str(row.currency),
value: dbAmount(row.value),
denominationFamilySerial: num(row.denomination_family_serial),
@@ -757,7 +812,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
isOffered: dbToBool(row.is_offered),
isRevoked: dbToBool(row.is_revoked),
isLost: dbToOptBool(row.is_lost),
- masterSig: str(row.master_sig),
+ masterSig: dbToCrock(row.master_sig),
verificationStatus: num(row.verification_status),
};
return denom;
@@ -884,7 +939,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
exec: rec.executionTime,
obt: rec.obtainedTime,
amt: rec.refundAmount,
- coin: rec.coinPub,
+ coin: crockToDb(rec.coinPub),
rtxid: rec.rtxid,
};
if (id !== undefined) {
@@ -903,7 +958,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletRefundItem | undefined> {
const row = await this.first(
"SELECT * FROM refund_items WHERE coin_pub = $coin AND rtxid = $rtxid",
- { coin: coinPub, rtxid },
+ { coin: crockToDb(coinPub), rtxid },
);
return row ? this.rowToRefundItem(row) : undefined;
}
@@ -917,7 +972,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
executionTime: dbTimestamp(row.execution_time),
obtainedTime: dbTimestamp(row.obtained_time),
refundAmount: dbAmount(row.refund_amount),
- coinPub: str(row.coin_pub),
+ coinPub: dbToCrock(row.coin_pub),
rtxid: num(row.rtxid),
};
}
@@ -926,13 +981,13 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToCoin(row: ResultRow): WalletCoin {
return {
- coinPub: str(row.coin_pub),
- coinPriv: str(row.coin_priv),
+ coinPub: dbToCrock(row.coin_pub),
+ coinPriv: dbToCrock(row.coin_priv),
exchangeBaseUrl: str(row.exchange_base_url),
- denomPubHash: str(row.denom_pub_hash),
+ denomPubHash: dbToCrock(row.denom_pub_hash),
denomSig: dbToJson(row.denom_sig),
- blindingKey: str(row.blinding_key),
- coinEvHash: str(row.coin_ev_hash),
+ blindingKey: dbToCrock(row.blinding_key),
+ coinEvHash: dbToCrock(row.coin_ev_hash),
status: str(row.status) as CoinStatus,
maxAge: num(row.max_age),
// Always set, even when absent: the field is declared as
@@ -948,7 +1003,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async getCoin(coinPub: string): Promise<WalletCoin | undefined> {
const row = await this.first("SELECT * FROM coins WHERE coin_pub = $pub", {
- pub: coinPub,
+ pub: crockToDb(coinPub),
});
return row ? this.rowToCoin(row) : undefined;
}
@@ -977,13 +1032,13 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
coin_source = excluded.coin_source,
source_transaction_id = excluded.source_transaction_id`,
{
- pub: coin.coinPub,
- priv: coin.coinPriv,
+ pub: crockToDb(coin.coinPub),
+ priv: crockToDb(coin.coinPriv),
url: coin.exchangeBaseUrl,
- dph: coin.denomPubHash,
+ dph: crockToDb(coin.denomPubHash),
sig: jsonToDb(coin.denomSig),
- bk: coin.blindingKey,
- ceh: coin.coinEvHash,
+ bk: crockToDb(coin.blindingKey),
+ ceh: crockToDb(coin.coinEvHash),
status: coin.status,
visible: coin.visible ?? null,
age: coin.maxAge,
@@ -1021,7 +1076,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async getCoinsByDenomPubHash(denomPubHash: string): Promise<WalletCoin[]> {
const rows = await this.all(
"SELECT * FROM coins WHERE denom_pub_hash = $dph",
- { dph: denomPubHash },
+ { dph: crockToDb(denomPubHash) },
);
return rows.map((r) => this.rowToCoin(r));
}
@@ -1045,23 +1100,33 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
if (coinPubs.length === 0) {
return [];
}
- const params: Record<string, string> = {};
- const placeholders = coinPubs.map((pub, i) => {
- params[`p${i}`] = pub;
+ const params: Record<string, Uint8Array> = {};
+ const blobs = coinPubs.map((pub) => crockToDb(pub));
+ const placeholders = blobs.map((blob, i) => {
+ params[`p${i}`] = blob;
return `$p${i}`;
});
const rows = await this.all(
`SELECT * FROM coins WHERE coin_pub IN (${placeholders.join(", ")})`,
params,
);
- const byPub = new Map<string, WalletCoin>();
+ // Keyed on the stored bytes, not on the caller's string. Several
+ // distinct strings can decode to the same key -- 52 Crockford characters
+ // carry 260 bits and a key is 256 -- so re-encoding a row yields the
+ // canonical spelling, which need not be the spelling the caller passed.
+ // Matching on the string dropped every coin whose argument was not
+ // canonical, silently and without error.
+ const byKey = new Map<string, WalletCoin>();
for (const row of rows) {
- const coin = this.rowToCoin(row);
- byPub.set(coin.coinPub, coin);
+ const raw = row.coin_pub;
+ if (!(raw instanceof Uint8Array)) {
+ throw Error("coins.coin_pub must be a BLOB column");
+ }
+ byKey.set(blobKey(raw), this.rowToCoin(row));
}
const coins: WalletCoin[] = [];
- for (const pub of coinPubs) {
- const coin = byPub.get(pub);
+ for (const blob of blobs) {
+ const coin = byKey.get(blobKey(blob));
if (coin) {
coins.push(coin);
}
@@ -1082,7 +1147,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
" LIMIT $limit",
{
url: exchangeBaseUrl,
- dph: denomPubHash,
+ dph: crockToDb(denomPubHash),
age: maxAge,
status: CoinStatus.Fresh,
limit,
@@ -1092,7 +1157,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
}
async deleteCoin(coinPub: string): Promise<void> {
- await this.run("DELETE FROM coins WHERE coin_pub = $pub", { pub: coinPub });
+ await this.run("DELETE FROM coins WHERE coin_pub = $pub", {
+ pub: crockToDb(coinPub),
+ });
}
// ------------------------------------------------------ coin history
@@ -1102,13 +1169,13 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletCoinHistory | undefined> {
const row = await this.first(
"SELECT * FROM coin_history WHERE coin_pub = $pub",
- { pub: coinPub },
+ { pub: crockToDb(coinPub) },
);
if (!row) {
return undefined;
}
return {
- coinPub: str(row.coin_pub),
+ coinPub: dbToCrock(row.coin_pub),
history: dbToJson(row.history),
};
}
@@ -1117,13 +1184,13 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
await this.run(
"INSERT INTO coin_history (coin_pub, history) VALUES ($pub, $h)" +
" ON CONFLICT(coin_pub) DO UPDATE SET history = excluded.history",
- { pub: rec.coinPub, h: jsonToDb(rec.history) },
+ { pub: crockToDb(rec.coinPub), h: jsonToDb(rec.history) },
);
}
async deleteCoinHistory(coinPub: string): Promise<void> {
await this.run("DELETE FROM coin_history WHERE coin_pub = $pub", {
- pub: coinPub,
+ pub: crockToDb(coinPub),
});
}
@@ -1132,14 +1199,14 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToCoinAvailability(row: ResultRow): WalletCoinAvailability {
return {
exchangeBaseUrl: str(row.exchange_base_url),
- denomPubHash: str(row.denom_pub_hash),
+ denomPubHash: dbToCrock(row.denom_pub_hash),
maxAge: num(row.max_age),
currency: str(row.currency),
value: dbAmount(row.value),
freshCoinCount: num(row.fresh_coin_count),
visibleCoinCount: num(row.visible_coin_count),
...(row.exchange_master_pub != null
- ? { exchangeMasterPub: str(row.exchange_master_pub) }
+ ? { exchangeMasterPub: dbToCrock(row.exchange_master_pub) }
: undefined),
...(row.pending_refresh_output_count != null
? { pendingRefreshOutputCount: num(row.pending_refresh_output_count) }
@@ -1156,7 +1223,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
"SELECT * FROM coin_availability" +
" WHERE exchange_base_url = $url AND denom_pub_hash = $dph" +
" AND max_age = $age",
- { url: exchangeBaseUrl, dph: denomPubHash, age: maxAge },
+ { url: exchangeBaseUrl, dph: crockToDb(denomPubHash), age: maxAge },
);
return row ? this.rowToCoinAvailability(row) : undefined;
}
@@ -1178,11 +1245,11 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
excluded.pending_refresh_output_count`,
{
url: rec.exchangeBaseUrl,
- dph: rec.denomPubHash,
+ dph: crockToDb(rec.denomPubHash),
age: rec.maxAge,
cur: rec.currency,
val: rec.value,
- emp: rec.exchangeMasterPub ?? null,
+ emp: optCrockToDb(rec.exchangeMasterPub),
fresh: rec.freshCoinCount,
vis: rec.visibleCoinCount,
pend: rec.pendingRefreshOutputCount ?? null,
@@ -1240,7 +1307,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
"DELETE FROM coin_availability" +
" WHERE exchange_base_url = $url AND denom_pub_hash = $dph" +
" AND max_age = $age",
- { url: exchangeBaseUrl, dph: denomPubHash, age: maxAge },
+ { url: exchangeBaseUrl, dph: crockToDb(denomPubHash), age: maxAge },
);
}
@@ -1254,7 +1321,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
row.details_pointer_master_pub == null
? undefined
: {
- masterPublicKey: str(row.details_pointer_master_pub),
+ masterPublicKey: dbToCrock(row.details_pointer_master_pub),
currency: str(row.details_pointer_currency),
updateClock: dbTimestamp(row.details_pointer_update_clock),
},
@@ -1293,10 +1360,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
? { currentMergeReserveRowId: num(row.current_merge_reserve_row_id) }
: undefined),
...(row.current_account_priv != null
- ? { currentAccountPriv: str(row.current_account_priv) }
+ ? { currentAccountPriv: dbToCrock(row.current_account_priv) }
: undefined),
...(row.current_account_pub != null
- ? { currentAccountPub: str(row.current_account_pub) }
+ ? { currentAccountPub: dbToCrock(row.current_account_pub) }
: undefined),
...(row.peer_payments_disabled != null
? { peerPaymentsDisabled: dbToBool(row.peer_payments_disabled) }
@@ -1375,7 +1442,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
: jsonToDb(rec.presetCurrencySpec),
pt: rec.presetType ?? null,
lw: rec.lastWithdrawal ?? null,
- dpmp: rec.detailsPointer?.masterPublicKey ?? null,
+ dpmp: optCrockToDb(rec.detailsPointer?.masterPublicKey),
dpc: rec.detailsPointer?.currency ?? null,
dpuc: rec.detailsPointer?.updateClock ?? null,
es: rec.entryStatus,
@@ -1393,8 +1460,8 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
lke: rec.lastKeysEtag ?? null,
nrcs: rec.nextRefreshCheckStamp,
cmrri: rec.currentMergeReserveRowId ?? null,
- cap: rec.currentAccountPriv ?? null,
- capub: rec.currentAccountPub ?? null,
+ cap: optCrockToDb(rec.currentAccountPriv),
+ capub: optCrockToDb(rec.currentAccountPub),
ppd: boolToDb(rec.peerPaymentsDisabled),
ddd: boolToDb(rec.directDepositDisabled),
nf: boolToDb(rec.noFees),
@@ -1414,7 +1481,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
return {
rowId: num(row.row_id),
exchangeBaseUrl: str(row.exchange_base_url),
- masterPublicKey: str(row.master_public_key),
+ masterPublicKey: dbToCrock(row.master_public_key),
currency: str(row.currency),
auditors: dbToJson(row.auditors),
protocolVersionRange: str(row.protocol_version_range),
@@ -1443,7 +1510,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private exchangeDetailsCols(rec: WalletExchangeDetails) {
return {
url: rec.exchangeBaseUrl,
- mpk: rec.masterPublicKey,
+ mpk: crockToDb(rec.masterPublicKey),
cur: rec.currency,
aud: jsonToDb(rec.auditors),
pvr: rec.protocolVersionRange,
@@ -1520,7 +1587,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
"SELECT * FROM exchange_details" +
" WHERE exchange_base_url = $url AND currency = $cur" +
" AND master_public_key = $mpk",
- { url: exchangeBaseUrl, cur: currency, mpk: masterPublicKey },
+ { url: exchangeBaseUrl, cur: currency, mpk: crockToDb(masterPublicKey) },
);
return row ? this.rowToExchangeDetails(row) : undefined;
}
@@ -1581,11 +1648,11 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
);
return rows.map((row) => ({
exchangeDetailsRowId: num(row.exchange_details_row_id),
- signkeyPub: str(row.signkey_pub),
+ signkeyPub: dbToCrock(row.signkey_pub),
stampStart: dbTimestamp(row.stamp_start),
stampExpire: dbTimestamp(row.stamp_expire),
stampEnd: dbTimestamp(row.stamp_end),
- masterSig: str(row.master_sig),
+ masterSig: dbToCrock(row.master_sig),
}));
}
@@ -1602,11 +1669,11 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
master_sig = excluded.master_sig`,
{
id: rec.exchangeDetailsRowId,
- pub: rec.signkeyPub,
+ pub: crockToDb(rec.signkeyPub),
start: rec.stampStart,
expire: rec.stampExpire,
end: rec.stampEnd,
- sig: rec.masterSig,
+ sig: crockToDb(rec.masterSig),
},
);
}
@@ -1618,7 +1685,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
await this.run(
"DELETE FROM exchange_sign_keys" +
" WHERE exchange_details_row_id = $id AND signkey_pub = $pub",
- { id: exchangeDetailsRowId, pub: signkeyPub },
+ { id: exchangeDetailsRowId, pub: crockToDb(signkeyPub) },
);
}
@@ -1629,7 +1696,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
denominationFamilySerial: num(row.denomination_family_serial),
familyParams: {
exchangeBaseUrl: str(row.exchange_base_url),
- exchangeMasterPub: str(row.exchange_master_pub),
+ exchangeMasterPub: dbToCrock(row.exchange_master_pub),
value: dbAmount(row.value),
feeWithdraw: dbAmount(row.fee_withdraw),
feeDeposit: dbAmount(row.fee_deposit),
@@ -1645,7 +1712,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
const p = rec.familyParams;
const cols = {
url: p.exchangeBaseUrl,
- mpub: p.exchangeMasterPub,
+ mpub: crockToDb(p.exchangeMasterPub),
val: p.value,
fw: p.feeWithdraw,
fd: p.feeDeposit,
@@ -1690,7 +1757,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
" AND fee_refresh = $frs AND fee_refund = $frf",
{
url: params.exchangeBaseUrl,
- mpub: params.exchangeMasterPub,
+ mpub: crockToDb(params.exchangeMasterPub),
val: params.value,
fw: params.feeWithdraw,
fd: params.feeDeposit,
@@ -1803,7 +1870,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
const cols = {
wtype: wgInfo.withdrawalType,
uri: null as string | null,
- cpriv: null as string | null,
+ cpriv: null as Uint8Array | null,
binfo: null as string | null,
eca: null as string | null,
};
@@ -1825,7 +1892,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
: jsonToDb(wgInfo.exchangeCreditAccounts);
break;
case WithdrawalRecordType.PeerPullCredit:
- cols.cpriv = wgInfo.contractPriv;
+ cols.cpriv = crockToDb(wgInfo.contractPriv);
break;
case WithdrawalRecordType.PeerPushCredit:
case WithdrawalRecordType.Recoup:
@@ -1870,7 +1937,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
case WithdrawalRecordType.PeerPullCredit: {
const wg: WgInfoBankPeerPull = {
withdrawalType: WithdrawalRecordType.PeerPullCredit,
- contractPriv: str(row.contract_priv),
+ contractPriv: dbToCrock(row.contract_priv),
};
return wg;
}
@@ -1893,16 +1960,16 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
return {
withdrawalGroupId: str(row.withdrawal_group_id),
wgInfo: this.rowToWgInfo(row),
- secretSeed: str(row.secret_seed),
- reservePub: str(row.reserve_pub),
- reservePriv: str(row.reserve_priv),
+ secretSeed: dbToCrock(row.secret_seed),
+ reservePub: dbToCrock(row.reserve_pub),
+ reservePriv: dbToCrock(row.reserve_priv),
timestampStart: dbTimestamp(row.timestamp_start),
status: num(row.status),
...(row.is_foreign_account != null
? { isForeignAccount: dbToBool(row.is_foreign_account) }
: undefined),
...(row.kyc_payto_hash != null
- ? { kycPaytoHash: str(row.kyc_payto_hash) }
+ ? { kycPaytoHash: dbToCrock(row.kyc_payto_hash) }
: undefined),
...(row.kyc_access_token != null
? { kycAccessToken: str(row.kyc_access_token) }
@@ -2025,7 +2092,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
id: rec.withdrawalGroupId,
...wg,
ifa: boolToDb(rec.isForeignAccount),
- kph: rec.kycPaytoHash ?? null,
+ kph: optCrockToDb(rec.kycPaytoHash),
kat: rec.kycAccessToken ?? null,
klcs: rec.kycLastCheckStatus ?? null,
klcc: rec.kycLastCheckCode ?? null,
@@ -2036,9 +2103,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
rec.kycWithdrawalDelay === undefined
? null
: jsonToDb(rec.kycWithdrawalDelay),
- seed: rec.secretSeed,
- rpub: rec.reservePub,
- rpriv: rec.reservePriv,
+ seed: crockToDb(rec.secretSeed),
+ rpub: crockToDb(rec.reservePub),
+ rpriv: crockToDb(rec.reservePriv),
url: rec.exchangeBaseUrl ?? null,
tstart: rec.timestampStart,
tfinish: rec.timestampFinish ?? null,
@@ -2119,17 +2186,17 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToPlanchet(row: ResultRow): WalletPlanchet {
return {
- coinPub: str(row.coin_pub),
- coinPriv: str(row.coin_priv),
+ coinPub: dbToCrock(row.coin_pub),
+ coinPriv: dbToCrock(row.coin_priv),
withdrawalGroupId: str(row.withdrawal_group_id),
coinIdx: num(row.coin_idx),
planchetStatus: num(row.planchet_status),
lastError: dbToOptJson(row.last_error),
- denomPubHash: str(row.denom_pub_hash),
- blindingKey: str(row.blinding_key),
- withdrawSig: str(row.withdraw_sig),
+ denomPubHash: dbToCrock(row.denom_pub_hash),
+ blindingKey: dbToCrock(row.blinding_key),
+ withdrawSig: dbToCrock(row.withdraw_sig),
coinEv: dbToJson(row.coin_ev),
- coinEvHash: str(row.coin_ev_hash),
+ coinEvHash: dbToCrock(row.coin_ev_hash),
...(row.age_commitment_proof != null
? { ageCommitmentProof: dbToJson(row.age_commitment_proof) }
: undefined),
@@ -2139,7 +2206,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async getPlanchet(coinPub: string): Promise<WalletPlanchet | undefined> {
const row = await this.first(
"SELECT * FROM planchets WHERE coin_pub = $pub",
- { pub: coinPub },
+ { pub: crockToDb(coinPub) },
);
return row ? this.rowToPlanchet(row) : undefined;
}
@@ -2167,17 +2234,17 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
coin_ev_hash = excluded.coin_ev_hash,
age_commitment_proof = excluded.age_commitment_proof`,
{
- pub: rec.coinPub,
- priv: rec.coinPriv,
+ pub: crockToDb(rec.coinPub),
+ priv: crockToDb(rec.coinPriv),
wgid: rec.withdrawalGroupId,
idx: rec.coinIdx,
status: rec.planchetStatus,
err: rec.lastError === undefined ? null : jsonToDb(rec.lastError),
- dph: rec.denomPubHash,
- bk: rec.blindingKey,
- sig: rec.withdrawSig,
+ dph: crockToDb(rec.denomPubHash),
+ bk: crockToDb(rec.blindingKey),
+ sig: crockToDb(rec.withdrawSig),
ev: jsonToDb(rec.coinEv),
- evh: rec.coinEvHash,
+ evh: crockToDb(rec.coinEvHash),
acp:
rec.ageCommitmentProof === undefined
? null
@@ -2218,7 +2285,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async deletePlanchet(coinPub: string): Promise<void> {
await this.run("DELETE FROM planchets WHERE coin_pub = $pub", {
- pub: coinPub,
+ pub: crockToDb(coinPub),
});
}
@@ -2358,17 +2425,17 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToPeerPushDebit(row: ResultRow): WalletPeerPushDebit {
return {
- pursePub: str(row.purse_pub),
+ pursePub: dbToCrock(row.purse_pub),
exchangeBaseUrl: str(row.exchange_base_url),
amount: dbAmount(row.amount),
totalCost: dbAmount(row.total_cost),
- contractTermsHash: str(row.contract_terms_hash),
- pursePriv: str(row.purse_priv),
- mergePub: str(row.merge_pub),
- mergePriv: str(row.merge_priv),
- contractPriv: str(row.contract_priv),
- contractPub: str(row.contract_pub),
- contractEncNonce: str(row.contract_enc_nonce),
+ contractTermsHash: dbToCrock(row.contract_terms_hash),
+ pursePriv: dbToCrock(row.purse_priv),
+ mergePub: dbToCrock(row.merge_pub),
+ mergePriv: dbToCrock(row.merge_priv),
+ contractPriv: dbToCrock(row.contract_priv),
+ contractPub: dbToCrock(row.contract_pub),
+ contractEncNonce: dbToCrock(row.contract_enc_nonce),
purseExpiration: dbTimestamp(row.purse_expiration),
timestampCreated: dbTimestamp(row.timestamp_created),
status: num(row.status),
@@ -2395,7 +2462,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletPeerPushDebit | undefined> {
const row = await this.first(
"SELECT * FROM peer_push_debit WHERE purse_pub = $pub",
- { pub: pursePub },
+ { pub: crockToDb(pursePub) },
);
return row ? this.rowToPeerPushDebit(row) : undefined;
}
@@ -2433,20 +2500,20 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
fail_reason = excluded.fail_reason,
status = excluded.status`,
{
- pub: rec.pursePub,
+ pub: crockToDb(rec.pursePub),
url: rec.exchangeBaseUrl,
scope:
rec.restrictScope === undefined ? null : jsonToDb(rec.restrictScope),
amt: rec.amount,
cost: rec.totalCost,
csel: rec.coinSel === undefined ? null : jsonToDb(rec.coinSel),
- cth: rec.contractTermsHash,
- ppriv: rec.pursePriv,
- mpub: rec.mergePub,
- mpriv: rec.mergePriv,
- cpriv: rec.contractPriv,
- cpub: rec.contractPub,
- nonce: rec.contractEncNonce,
+ cth: crockToDb(rec.contractTermsHash),
+ ppriv: crockToDb(rec.pursePriv),
+ mpub: crockToDb(rec.mergePub),
+ mpriv: crockToDb(rec.mergePriv),
+ cpriv: crockToDb(rec.contractPriv),
+ cpub: crockToDb(rec.contractPub),
+ nonce: crockToDb(rec.contractEncNonce),
exp: rec.purseExpiration,
created: rec.timestampCreated,
argi: rec.abortRefreshGroupId ?? null,
@@ -2459,7 +2526,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async deletePeerPushDebit(pursePub: string): Promise<void> {
await this.run("DELETE FROM peer_push_debit WHERE purse_pub = $pub", {
- pub: pursePub,
+ pub: crockToDb(pursePub),
});
}
@@ -2485,12 +2552,12 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
return {
peerPushCreditId: str(row.peer_push_credit_id),
exchangeBaseUrl: str(row.exchange_base_url),
- pursePub: str(row.purse_pub),
- mergePriv: str(row.merge_priv),
- contractPriv: str(row.contract_priv),
+ pursePub: dbToCrock(row.purse_pub),
+ mergePriv: dbToCrock(row.merge_priv),
+ contractPriv: dbToCrock(row.contract_priv),
timestamp: dbTimestamp(row.timestamp),
estimatedAmountEffective: dbAmount(row.estimated_amount_effective),
- contractTermsHash: str(row.contract_terms_hash),
+ contractTermsHash: dbToCrock(row.contract_terms_hash),
status: num(row.status),
withdrawalGroupId: optStr(row.withdrawal_group_id),
currency: optStr(row.currency),
@@ -2501,7 +2568,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
? { failReason: dbToJson(row.fail_reason) }
: undefined),
...(row.kyc_payto_hash != null
- ? { kycPaytoHash: str(row.kyc_payto_hash) }
+ ? { kycPaytoHash: dbToCrock(row.kyc_payto_hash) }
: undefined),
...(row.kyc_access_token != null
? { kycAccessToken: str(row.kyc_access_token) }
@@ -2571,18 +2638,18 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
{
id: rec.peerPushCreditId,
url: rec.exchangeBaseUrl,
- ppub: rec.pursePub,
- mpriv: rec.mergePriv,
- cpriv: rec.contractPriv,
+ ppub: crockToDb(rec.pursePub),
+ mpriv: crockToDb(rec.mergePriv),
+ cpriv: crockToDb(rec.contractPriv),
ts: rec.timestamp,
eae: rec.estimatedAmountEffective,
- cth: rec.contractTermsHash,
+ cth: crockToDb(rec.contractTermsHash),
status: rec.status,
abort: rec.abortReason === undefined ? null : jsonToDb(rec.abortReason),
fail: rec.failReason === undefined ? null : jsonToDb(rec.failReason),
wgid: rec.withdrawalGroupId ?? null,
cur: rec.currency ?? null,
- kph: rec.kycPaytoHash ?? null,
+ kph: optCrockToDb(rec.kycPaytoHash),
kat: rec.kycAccessToken ?? null,
klcs: rec.kycLastCheckStatus ?? null,
klcc: rec.kycLastCheckCode ?? null,
@@ -2623,7 +2690,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
const row = await this.first(
"SELECT * FROM peer_push_credit" +
" WHERE exchange_base_url = $url AND contract_priv = $priv",
- { url: exchangeBaseUrl, priv: contractPriv },
+ { url: exchangeBaseUrl, priv: crockToDb(contractPriv) },
);
return row ? this.rowToPeerPushCredit(row) : undefined;
}
@@ -2633,12 +2700,12 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToPeerPullDebit(row: ResultRow): WalletPeerPullDebit {
return {
peerPullDebitId: str(row.peer_pull_debit_id),
- pursePub: str(row.purse_pub),
+ pursePub: dbToCrock(row.purse_pub),
exchangeBaseUrl: str(row.exchange_base_url),
amount: dbAmount(row.amount),
- contractTermsHash: str(row.contract_terms_hash),
+ contractTermsHash: dbToCrock(row.contract_terms_hash),
timestampCreated: dbTimestamp(row.timestamp_created),
- contractPriv: str(row.contract_priv),
+ contractPriv: dbToCrock(row.contract_priv),
status: num(row.status),
totalCostEstimated: dbAmount(row.total_cost_estimated),
...(row.abort_refresh_group_id != null
@@ -2692,12 +2759,12 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
coin_sel = excluded.coin_sel`,
{
id: rec.peerPullDebitId,
- ppub: rec.pursePub,
+ ppub: crockToDb(rec.pursePub),
url: rec.exchangeBaseUrl,
amt: rec.amount,
- cth: rec.contractTermsHash,
+ cth: crockToDb(rec.contractTermsHash),
created: rec.timestampCreated,
- cpriv: rec.contractPriv,
+ cpriv: crockToDb(rec.contractPriv),
status: rec.status,
tce: rec.totalCostEstimated,
argi: rec.abortRefreshGroupId ?? null,
@@ -2727,7 +2794,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
const row = await this.first(
"SELECT * FROM peer_pull_debit" +
" WHERE exchange_base_url = $url AND contract_priv = $priv",
- { url: exchangeBaseUrl, priv: contractPriv },
+ { url: exchangeBaseUrl, priv: crockToDb(contractPriv) },
);
return row ? this.rowToPeerPullDebit(row) : undefined;
}
@@ -2736,23 +2803,23 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToPeerPullCredit(row: ResultRow): WalletPeerPullCredit {
return {
- pursePub: str(row.purse_pub),
+ pursePub: dbToCrock(row.purse_pub),
exchangeBaseUrl: str(row.exchange_base_url),
amount: dbAmount(row.amount),
estimatedAmountEffective: dbAmount(row.estimated_amount_effective),
- pursePriv: str(row.purse_priv),
- contractTermsHash: str(row.contract_terms_hash),
- mergePub: str(row.merge_pub),
- mergePriv: str(row.merge_priv),
- contractPub: str(row.contract_pub),
- contractPriv: str(row.contract_priv),
- contractEncNonce: str(row.contract_enc_nonce),
+ pursePriv: dbToCrock(row.purse_priv),
+ contractTermsHash: dbToCrock(row.contract_terms_hash),
+ mergePub: dbToCrock(row.merge_pub),
+ mergePriv: dbToCrock(row.merge_priv),
+ contractPub: dbToCrock(row.contract_pub),
+ contractPriv: dbToCrock(row.contract_priv),
+ contractEncNonce: dbToCrock(row.contract_enc_nonce),
mergeTimestamp: dbTimestamp(row.merge_timestamp),
mergeReserveRowId: num(row.merge_reserve_row_id),
status: num(row.status),
withdrawalGroupId: optStr(row.withdrawal_group_id),
...(row.kyc_payto_hash != null
- ? { kycPaytoHash: str(row.kyc_payto_hash) }
+ ? { kycPaytoHash: dbToCrock(row.kyc_payto_hash) }
: undefined),
...(row.kyc_access_token != null
? { kycAccessToken: str(row.kyc_access_token) }
@@ -2786,7 +2853,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletPeerPullCredit | undefined> {
const row = await this.first(
"SELECT * FROM peer_pull_credit WHERE purse_pub = $pub",
- { pub: pursePub },
+ { pub: crockToDb(pursePub) },
);
return row ? this.rowToPeerPullCredit(row) : undefined;
}
@@ -2831,21 +2898,21 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
fail_reason = excluded.fail_reason,
withdrawal_group_id = excluded.withdrawal_group_id`,
{
- pub: rec.pursePub,
+ pub: crockToDb(rec.pursePub),
url: rec.exchangeBaseUrl,
amt: rec.amount,
eae: rec.estimatedAmountEffective,
- ppriv: rec.pursePriv,
- cth: rec.contractTermsHash,
- mpub: rec.mergePub,
- mpriv: rec.mergePriv,
- cpub: rec.contractPub,
- cpriv: rec.contractPriv,
- nonce: rec.contractEncNonce,
+ ppriv: crockToDb(rec.pursePriv),
+ cth: crockToDb(rec.contractTermsHash),
+ mpub: crockToDb(rec.mergePub),
+ mpriv: crockToDb(rec.mergePriv),
+ cpub: crockToDb(rec.contractPub),
+ cpriv: crockToDb(rec.contractPriv),
+ nonce: crockToDb(rec.contractEncNonce),
mts: rec.mergeTimestamp,
mrri: rec.mergeReserveRowId,
status: rec.status,
- kph: rec.kycPaytoHash ?? null,
+ kph: optCrockToDb(rec.kycPaytoHash),
kat: rec.kycAccessToken ?? null,
klcs: rec.kycLastCheckStatus ?? null,
klcc: rec.kycLastCheckCode ?? null,
@@ -2861,7 +2928,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async deletePeerPullCredit(pursePub: string): Promise<void> {
await this.run("DELETE FROM peer_pull_credit WHERE purse_pub = $pub", {
- pub: pursePub,
+ pub: crockToDb(pursePub),
});
}
@@ -2878,12 +2945,12 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
currency: str(row.currency),
amount: dbAmount(row.amount),
wireTransferDeadline: dbTimestamp(row.wire_transfer_deadline),
- merchantPub: str(row.merchant_pub),
- merchantPriv: str(row.merchant_priv),
- noncePriv: str(row.nonce_priv),
- noncePub: str(row.nonce_pub),
+ merchantPub: dbToCrock(row.merchant_pub),
+ merchantPriv: dbToCrock(row.merchant_priv),
+ noncePriv: dbToCrock(row.nonce_priv),
+ noncePub: dbToCrock(row.nonce_pub),
wire: dbToJson(row.wire),
- contractTermsHash: str(row.contract_terms_hash),
+ contractTermsHash: dbToCrock(row.contract_terms_hash),
totalPayCost: dbAmount(row.total_pay_cost),
counterpartyEffectiveDepositAmount: dbAmount(
row.counterparty_effective_deposit_amount,
@@ -2996,12 +3063,12 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
cur: rec.currency,
amt: rec.amount,
wtd: rec.wireTransferDeadline,
- mpub: rec.merchantPub,
- mpriv: rec.merchantPriv,
- npriv: rec.noncePriv,
- npub: rec.noncePub,
+ mpub: crockToDb(rec.merchantPub),
+ mpriv: crockToDb(rec.merchantPriv),
+ npriv: crockToDb(rec.noncePriv),
+ npub: crockToDb(rec.noncePub),
wire: jsonToDb(rec.wire),
- cth: rec.contractTermsHash,
+ cth: crockToDb(rec.contractTermsHash),
pcs:
rec.payCoinSelection === undefined
? null
@@ -3275,16 +3342,16 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
downloadSessionId: optStr(row.download_session_id),
repurchaseProposalId: optStr(row.repurchase_proposal_id),
purchaseStatus: num(row.purchase_status),
- noncePriv: str(row.nonce_priv),
- noncePub: str(row.nonce_pub),
- secretSeed: optStr(row.secret_seed),
+ noncePriv: dbToCrock(row.nonce_priv),
+ noncePub: dbToCrock(row.nonce_pub),
+ secretSeed: dbToOptCrock(row.secret_seed),
download,
payInfo: dbToOptJson(row.pay_info),
timestampFirstSuccessfulPay:
row.timestamp_first_successful_pay == null
? undefined
: dbTimestamp(row.timestamp_first_successful_pay),
- merchantPaySig: optStr(row.merchant_pay_sig),
+ merchantPaySig: dbToOptCrock(row.merchant_pay_sig),
posConfirmation: optStr(row.pos_confirmation),
shared: dbToBool(row.shared),
timestamp: dbTimestamp(row.timestamp),
@@ -3331,7 +3398,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
? { donauAmount: dbAmount(row.donau_amount) }
: undefined),
...(row.donau_tax_id_hash != null
- ? { donauTaxIdHash: str(row.donau_tax_id_hash) }
+ ? { donauTaxIdHash: dbToCrock(row.donau_tax_id_hash) }
: undefined),
...(row.donau_tax_id_salt != null
? { donauTaxIdSalt: str(row.donau_tax_id_salt) }
@@ -3468,10 +3535,10 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
argi: rec.abortRefreshGroupId ?? null,
abort: rec.abortReason === undefined ? null : jsonToDb(rec.abortReason),
fail: rec.failReason === undefined ? null : jsonToDb(rec.failReason),
- npriv: rec.noncePriv,
- npub: rec.noncePub,
+ npriv: crockToDb(rec.noncePriv),
+ npub: crockToDb(rec.noncePub),
ci: rec.choiceIndex ?? null,
- seed: rec.secretSeed ?? null,
+ seed: optCrockToDb(rec.secretSeed),
dl: downloadJson,
ffu: fulfillmentUrl,
pi: rec.payInfo === undefined ? null : jsonToDb(rec.payInfo),
@@ -3480,12 +3547,12 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
? null
: jsonToDb(rec.pendingRemovedCoinPubs),
tfsp: rec.timestampFirstSuccessfulPay ?? null,
- mps: rec.merchantPaySig ?? null,
+ mps: optCrockToDb(rec.merchantPaySig),
posc: rec.posConfirmation ?? null,
doi: rec.donauOutputIndex ?? null,
dbu: rec.donauBaseUrl ?? null,
damt: rec.donauAmount ?? null,
- dtih: rec.donauTaxIdHash ?? null,
+ dtih: optCrockToDb(rec.donauTaxIdHash),
dtis: rec.donauTaxIdSalt ?? null,
dti: rec.donauTaxId ?? null,
dy: rec.donauYear ?? null,
@@ -3665,17 +3732,17 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToDonationPlanchet(row: ResultRow): WalletDonationPlanchet {
return {
- udiNonce: str(row.udi_nonce),
+ udiNonce: dbToCrock(row.udi_nonce),
donauBaseUrl: str(row.donau_base_url),
- donorTaxIdHash: str(row.donor_tax_id_hash),
+ donorTaxIdHash: dbToCrock(row.donor_tax_id_hash),
donorHashSalt: str(row.donor_hash_salt),
donorTaxId: str(row.donor_tax_id),
donationYear: num(row.donation_year),
proposalId: str(row.proposal_id),
udiIndex: num(row.udi_index),
blindedUdi: dbToJson(row.blinded_udi),
- bks: str(row.bks),
- donationUnitPubHash: str(row.donation_unit_pub_hash),
+ bks: dbToCrock(row.bks),
+ donationUnitPubHash: dbToCrock(row.donation_unit_pub_hash),
value: dbAmount(row.value),
};
}
@@ -3703,17 +3770,17 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
donation_unit_pub_hash = excluded.donation_unit_pub_hash,
value = excluded.value`,
{
- nonce: rec.udiNonce,
+ nonce: crockToDb(rec.udiNonce),
url: rec.donauBaseUrl,
- dtih: rec.donorTaxIdHash,
+ dtih: crockToDb(rec.donorTaxIdHash),
dhs: rec.donorHashSalt,
dti: rec.donorTaxId,
year: rec.donationYear,
pid: rec.proposalId,
idx: rec.udiIndex,
budi: jsonToDb(rec.blindedUdi),
- bks: rec.bks,
- duph: rec.donationUnitPubHash,
+ bks: crockToDb(rec.bks),
+ duph: crockToDb(rec.donationUnitPubHash),
val: rec.value,
},
);
@@ -3739,14 +3806,14 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToDonationReceipt(row: ResultRow): WalletDonationReceipt {
return {
- udiNonce: str(row.udi_nonce),
+ udiNonce: dbToCrock(row.udi_nonce),
status: num(row.status),
donauBaseUrl: str(row.donau_base_url),
proposalId: str(row.proposal_id),
donationYear: num(row.donation_year),
- donationUnitPubHash: str(row.donation_unit_pub_hash),
+ donationUnitPubHash: dbToCrock(row.donation_unit_pub_hash),
donationUnitSig: dbToJson(row.donation_unit_sig),
- donorTaxIdHash: str(row.donor_tax_id_hash),
+ donorTaxIdHash: dbToCrock(row.donor_tax_id_hash),
donorHashSalt: str(row.donor_hash_salt),
donorTaxId: str(row.donor_tax_id),
value: dbAmount(row.value),
@@ -3759,7 +3826,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletDonationReceipt | undefined> {
const row = await this.first(
"SELECT * FROM donation_receipts WHERE udi_nonce = $nonce",
- { nonce: udiNonce },
+ { nonce: crockToDb(udiNonce) },
);
return row ? this.rowToDonationReceipt(row) : undefined;
}
@@ -3787,14 +3854,14 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
value = excluded.value,
udi_index = excluded.udi_index`,
{
- nonce: rec.udiNonce,
+ nonce: crockToDb(rec.udiNonce),
status: rec.status,
url: rec.donauBaseUrl,
pid: rec.proposalId,
year: rec.donationYear,
- duph: rec.donationUnitPubHash,
+ duph: crockToDb(rec.donationUnitPubHash),
dus: jsonToDb(rec.donationUnitSig),
- dtih: rec.donorTaxIdHash,
+ dtih: crockToDb(rec.donorTaxIdHash),
dhs: rec.donorHashSalt,
dti: rec.donorTaxId,
val: rec.value,
@@ -3988,7 +4055,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
id: num(row.id),
currency: str(row.currency),
exchangeBaseUrl: str(row.exchange_base_url),
- exchangeMasterPub: str(row.exchange_master_pub),
+ exchangeMasterPub: dbToCrock(row.exchange_master_pub),
}));
}
@@ -4002,7 +4069,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
{
cur: rec.currency,
url: rec.exchangeBaseUrl,
- pub: rec.exchangeMasterPub,
+ pub: crockToDb(rec.exchangeMasterPub),
},
);
}
@@ -4022,7 +4089,11 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
"SELECT * FROM global_currency_exchanges" +
" WHERE currency = $cur AND exchange_base_url = $url" +
" AND exchange_master_pub = $pub",
- { cur: currency, url: exchangeBaseUrl, pub: exchangeMasterPub },
+ {
+ cur: currency,
+ url: exchangeBaseUrl,
+ pub: crockToDb(exchangeMasterPub),
+ },
);
if (!row) {
return undefined;
@@ -4031,7 +4102,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
id: num(row.id),
currency: str(row.currency),
exchangeBaseUrl: str(row.exchange_base_url),
- exchangeMasterPub: str(row.exchange_master_pub),
+ exchangeMasterPub: dbToCrock(row.exchange_master_pub),
};
}
@@ -4041,7 +4112,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
id: num(row.id),
currency: str(row.currency),
auditorBaseUrl: str(row.auditor_base_url),
- auditorPub: str(row.auditor_pub),
+ auditorPub: dbToCrock(row.auditor_pub),
}));
}
@@ -4052,7 +4123,11 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
"INSERT OR IGNORE INTO global_currency_auditors" +
" (currency, auditor_base_url, auditor_pub)" +
" VALUES ($cur, $url, $pub)",
- { cur: rec.currency, url: rec.auditorBaseUrl, pub: rec.auditorPub },
+ {
+ cur: rec.currency,
+ url: rec.auditorBaseUrl,
+ pub: crockToDb(rec.auditorPub),
+ },
);
}
@@ -4071,7 +4146,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
"SELECT * FROM global_currency_auditors" +
" WHERE currency = $cur AND auditor_base_url = $url" +
" AND auditor_pub = $pub",
- { cur: currency, url: auditorBaseUrl, pub: auditorPub },
+ { cur: currency, url: auditorBaseUrl, pub: crockToDb(auditorPub) },
);
if (!row) {
return undefined;
@@ -4080,7 +4155,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
id: num(row.id),
currency: str(row.currency),
auditorBaseUrl: str(row.auditor_base_url),
- auditorPub: str(row.auditor_pub),
+ auditorPub: dbToCrock(row.auditor_pub),
};
}
@@ -4165,19 +4240,19 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToToken(row: ResultRow): WalletToken {
return {
- tokenUsePub: str(row.token_use_pub),
- tokenUsePriv: str(row.token_use_priv),
+ tokenUsePub: dbToCrock(row.token_use_pub),
+ tokenUsePriv: dbToCrock(row.token_use_priv),
purchaseId: str(row.purchase_id),
merchantBaseUrl: str(row.merchant_base_url),
kind: str(row.kind) as MerchantContractTokenKind,
- tokenIssuePubHash: str(row.token_issue_pub_hash),
+ tokenIssuePubHash: dbToCrock(row.token_issue_pub_hash),
validAfter: dbTimestamp(row.valid_after),
validBefore: dbTimestamp(row.valid_before),
tokenIssueSig: dbToJson(row.token_issue_sig),
tokenUseSig: dbToOptJson(row.token_use_sig),
tokenEv: dbToJson(row.token_ev),
- tokenEvHash: str(row.token_ev_hash),
- blindingKey: str(row.blinding_key),
+ tokenEvHash: dbToCrock(row.token_ev_hash),
+ blindingKey: dbToCrock(row.blinding_key),
slug: str(row.slug),
name: str(row.name),
description: str(row.description),
@@ -4197,7 +4272,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
? { repeatIndex: num(row.repeat_index) }
: undefined),
...(row.token_family_hash != null
- ? { tokenFamilyHash: str(row.token_family_hash) }
+ ? { tokenFamilyHash: dbToCrock(row.token_family_hash) }
: undefined),
};
}
@@ -4210,7 +4285,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async getToken(tokenUsePub: string): Promise<WalletToken | undefined> {
const row = await this.first(
"SELECT * FROM tokens WHERE token_use_pub = $pub",
- { pub: tokenUsePub },
+ { pub: crockToDb(tokenUsePub) },
);
return row ? this.rowToToken(row) : undefined;
}
@@ -4253,8 +4328,8 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
token_issue_pub = excluded.token_issue_pub,
description_i18n = excluded.description_i18n`,
{
- pub: rec.tokenUsePub,
- priv: rec.tokenUsePriv,
+ pub: crockToDb(rec.tokenUsePub),
+ priv: crockToDb(rec.tokenUsePriv),
pid: rec.purchaseId,
tid: rec.transactionId ?? null,
ci: rec.choiceIndex ?? null,
@@ -4262,15 +4337,15 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
ri: rec.repeatIndex ?? null,
url: rec.merchantBaseUrl,
kind: rec.kind,
- tiph: rec.tokenIssuePubHash,
- tfh: rec.tokenFamilyHash ?? null,
+ tiph: crockToDb(rec.tokenIssuePubHash),
+ tfh: optCrockToDb(rec.tokenFamilyHash),
va: rec.validAfter,
vb: rec.validBefore,
sig: jsonToDb(rec.tokenIssueSig),
usig: rec.tokenUseSig === undefined ? null : jsonToDb(rec.tokenUseSig),
ev: jsonToDb(rec.tokenEv),
- evh: rec.tokenEvHash,
- bk: rec.blindingKey,
+ evh: crockToDb(rec.tokenEvHash),
+ bk: crockToDb(rec.blindingKey),
slug: rec.slug,
name: rec.name,
desc: rec.description,
@@ -4286,7 +4361,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async deleteToken(tokenUsePub: string): Promise<void> {
await this.run("DELETE FROM tokens WHERE token_use_pub = $pub", {
- pub: tokenUsePub,
+ pub: crockToDb(tokenUsePub),
});
}
@@ -4295,7 +4370,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
): Promise<WalletToken[]> {
const rows = await this.all(
"SELECT * FROM tokens WHERE token_issue_pub_hash = $h",
- { h: tokenIssuePubHash },
+ { h: crockToDb(tokenIssuePubHash) },
);
return rows.map((r) => this.rowToToken(r));
}
@@ -4304,18 +4379,18 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
private rowToSlate(row: ResultRow): WalletSlate {
return {
- tokenUsePub: str(row.token_use_pub),
- tokenUsePriv: str(row.token_use_priv),
+ tokenUsePub: dbToCrock(row.token_use_pub),
+ tokenUsePriv: dbToCrock(row.token_use_priv),
purchaseId: str(row.purchase_id),
merchantBaseUrl: str(row.merchant_base_url),
kind: str(row.kind) as MerchantContractTokenKind,
- tokenIssuePubHash: str(row.token_issue_pub_hash),
+ tokenIssuePubHash: dbToCrock(row.token_issue_pub_hash),
validAfter: dbTimestamp(row.valid_after),
validBefore: dbTimestamp(row.valid_before),
tokenUseSig: dbToOptJson(row.token_use_sig),
tokenEv: dbToJson(row.token_ev),
- tokenEvHash: str(row.token_ev_hash),
- blindingKey: str(row.blinding_key),
+ tokenEvHash: dbToCrock(row.token_ev_hash),
+ blindingKey: dbToCrock(row.blinding_key),
slug: str(row.slug),
name: str(row.name),
description: str(row.description),
@@ -4335,7 +4410,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
? { repeatIndex: num(row.repeat_index) }
: undefined),
...(row.token_family_hash != null
- ? { tokenFamilyHash: str(row.token_family_hash) }
+ ? { tokenFamilyHash: dbToCrock(row.token_family_hash) }
: undefined),
};
}
@@ -4402,8 +4477,8 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
token_issue_pub = excluded.token_issue_pub,
description_i18n = excluded.description_i18n`,
{
- pub: rec.tokenUsePub,
- priv: rec.tokenUsePriv,
+ pub: crockToDb(rec.tokenUsePub),
+ priv: crockToDb(rec.tokenUsePriv),
pid: rec.purchaseId,
tid: rec.transactionId ?? null,
ci: rec.choiceIndex ?? null,
@@ -4411,14 +4486,14 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
ri: rec.repeatIndex ?? null,
url: rec.merchantBaseUrl,
kind: rec.kind,
- tiph: rec.tokenIssuePubHash,
- tfh: rec.tokenFamilyHash ?? null,
+ tiph: crockToDb(rec.tokenIssuePubHash),
+ tfh: optCrockToDb(rec.tokenFamilyHash),
va: rec.validAfter,
vb: rec.validBefore,
usig: rec.tokenUseSig === undefined ? null : jsonToDb(rec.tokenUseSig),
ev: jsonToDb(rec.tokenEv),
- evh: rec.tokenEvHash,
- bk: rec.blindingKey,
+ evh: crockToDb(rec.tokenEvHash),
+ bk: crockToDb(rec.blindingKey),
slug: rec.slug,
name: rec.name,
desc: rec.description,
@@ -4434,7 +4509,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
async deleteSlate(tokenUsePub: string): Promise<void> {
await this.run("DELETE FROM slates WHERE token_use_pub = $pub", {
- pub: tokenUsePub,
+ pub: crockToDb(tokenUsePub),
});
}
@@ -4447,7 +4522,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
amountRefreshOutput: dbAmount(row.amount_refresh_output),
newDenoms: dbToJson(row.new_denoms),
...(row.session_public_seed != null
- ? { sessionPublicSeed: str(row.session_public_seed) }
+ ? { sessionPublicSeed: dbToCrock(row.session_public_seed) }
: undefined),
...(row.noreveal_index != null
? { norevealIndex: num(row.noreveal_index) }
@@ -4485,7 +4560,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction {
{
id: rec.refreshGroupId,
idx: rec.coinIndex,
- seed: rec.sessionPublicSeed ?? null,
+ seed: optCrockToDb(rec.sessionPublicSeed),
amt: rec.amountRefreshOutput,
nd: jsonToDb(rec.newDenoms),
nri: rec.norevealIndex ?? null,
@@ -4872,7 +4947,29 @@ async function listDataTables(ndb: NativeSqliteWalletDb): Promise<string[]> {
*/
export interface NativeSqliteDbDump {
schemaVersion: number;
- tables: Record<string, Record<string, Sqlite3Value>[]>;
+ tables: Record<string, Record<string, DumpValue>[]>;
+}
+
+/**
+ * A BLOB column in a dump.
+ *
+ * Tagged rather than raw bytes because the dump is stored in the IndexedDB
+ * backup database, and that layer cannot persist binary at all:
+ * structuredEncapsulate, which serialises values on their way to storage,
+ * handles arrays, dates, plain objects, bigint, boolean, number and string,
+ * and throws on anything else -- typed arrays included. (Its in-memory clone
+ * does support them, so this only bites on persistence.)
+ */
+export interface DumpBlob {
+ $blob: string;
+}
+
+export type DumpValue = string | number | null | DumpBlob;
+
+function isDumpBlob(v: unknown): v is DumpBlob {
+ return (
+ typeof v === "object" && v !== null && typeof (v as any).$blob === "string"
+ );
}
export async function exportNativeSqliteDb(
@@ -4888,13 +4985,19 @@ export async function exportNativeSqliteDb(
const rows = await (
await ndb.db.prepare(`SELECT * FROM "${table}"`)
).getAll();
- // The helper can return INTEGER columns as bigint, which JSON cannot
- // represent. Every integer in this schema (timestamps in
- // microseconds, row ids, statuses) is inside the safe range.
out.tables[table] = rows.map((row) => {
- const clean: Record<string, Sqlite3Value> = {};
+ const clean: Record<string, DumpValue> = {};
for (const [k, v] of Object.entries(row)) {
- clean[k] = typeof v === "bigint" ? Number(v) : v;
+ if (v instanceof Uint8Array) {
+ clean[k] = { $blob: encodeCrock(v) };
+ } else if (typeof v === "bigint") {
+ // The helper can return INTEGER columns as bigint, which JSON
+ // cannot represent. Every integer in this schema (timestamps in
+ // microseconds, row ids, statuses) is inside the safe range.
+ clean[k] = Number(v);
+ } else {
+ clean[k] = v;
+ }
}
return clean;
});
@@ -4931,7 +5034,8 @@ export async function importNativeSqliteDb(
const cols = Object.keys(row);
const params: Record<string, Sqlite3Value> = {};
for (const c of cols) {
- params[c] = row[c];
+ const v = row[c];
+ params[c] = isDumpBlob(v) ? decodeCrock(v.$blob) : v;
}
const colList = cols.map((c) => `"${c}"`).join(", ");
const valList = cols.map((c) => `$${c}`).join(", ");