Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions src/railgun-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
UnshieldStoredEvent,
} from './models/event-types';
import { ViewOnlyWallet } from './wallet/view-only-wallet';
import { HardwareWallet, type ExternalSignerConnector } from './wallet/hardware-wallet';
import { AbstractWallet } from './wallet/abstract-wallet';
import WalletInfo from './wallet/wallet-info';
import {
Expand Down Expand Up @@ -2115,14 +2116,42 @@ class RailgunEngine extends EventEmitter {
* @returns id
*/
async loadExistingViewOnlyWallet(encryptionKey: string, id: string): Promise<ViewOnlyWallet> {
if (isDefined(this.wallets[id])) {
return this.wallets[id] as ViewOnlyWallet;
const loadedWallet = this.wallets[id];
if (isDefined(loadedWallet)) {
if (loadedWallet instanceof ViewOnlyWallet) {
return loadedWallet;
}
this.unloadWallet(id);
}
const wallet = await ViewOnlyWallet.loadExisting(this.db, encryptionKey, id, this.prover);
await this.loadWallet(wallet);
return wallet;
}

async loadExistingHardwareWallet(
encryptionKey: string,
id: string,
connector: ExternalSignerConnector,
): Promise<HardwareWallet> {
const loadedWallet = this.wallets[id];
if (isDefined(loadedWallet)) {
if (loadedWallet instanceof HardwareWallet) {
loadedWallet.setConnector(connector);
return loadedWallet;
}
this.unloadWallet(id);
}
const wallet = await HardwareWallet.loadExisting(
this.db,
encryptionKey,
id,
this.prover,
);
wallet.setConnector(connector);
await this.loadWallet(wallet);
return wallet;
}

async deleteWallet(id: string) {
this.unloadWallet(id);
return AbstractWallet.delete(this.db, id);
Expand Down Expand Up @@ -2169,6 +2198,24 @@ class RailgunEngine extends EventEmitter {
return wallet;
}

async createHardwareWalletFromShareableViewingKey(
encryptionKey: string,
shareableViewingKey: string,
creationBlockNumbers: Optional<number[][]>,
connector: ExternalSignerConnector,
): Promise<HardwareWallet> {
const wallet = await HardwareWallet.fromShareableViewingKey(
this.db,
encryptionKey,
shareableViewingKey,
creationBlockNumbers,
this.prover,
);
wallet.setConnector(connector);
await this.loadWallet(wallet);
return wallet;
}

async getAllShieldCommitments(
txidVersion: TXIDVersion,
chain: Chain,
Expand Down
91 changes: 91 additions & 0 deletions src/transaction/__tests__/transaction-erc20.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { Database } from '../../database/database';
import { AddressData } from '../../key-derivation/bech32';
import { TransactNote } from '../../note/transact-note';
import { Prover, SnarkJSGroth16 } from '../../prover/prover';
import { type ExternalSignerConnector, HardwareWallet } from '../../wallet/hardware-wallet';
import { RailgunWallet } from '../../wallet/railgun-wallet';
import { config } from '../../test/config.test';
import { hashBoundParamsV2, hashBoundParamsV3 } from '../bound-params';
Expand Down Expand Up @@ -851,6 +852,96 @@ describe('transaction-erc20', function test() {
expect(signature).to.deep.equal(signEDDSA(privateKey, msg));
});

it('Should request one hardware wallet batch approval and sign with the returned sub-session', async () => {
transactionBatch.addOutput(await makeNote(1n));

const hardwareWallet = await HardwareWallet.fromShareableViewingKey(
db,
testEncryptionKey,
wallet.generateShareableViewingKey(),
undefined,
prover,
);
await hardwareWallet.loadUTXOMerkletree(txidVersion, utxoMerkletree);
await hardwareWallet.decryptBalances(txidVersion, chain, () => {}, false);
await hardwareWallet.refreshPOIsForTXIDVersion(chain, txidVersion, true);

const signedSubSessions: Optional<string>[] = [];
const expectedHashes: bigint[] = [];
let batchApprovalCalls = 0;
let approvedRequestCount = 0;
const connector: ExternalSignerConnector = {
requestBatchApproval: async (requests) => {
batchApprovalCalls += 1;
approvedRequestCount = requests.length;
return 'batch-sub-session';
},
sign: async (expectedHash, _publicInputs, subSession) => {
signedSubSessions.push(subSession);
expectedHashes.push(expectedHash);
return signEDDSA(
(await wallet.getSpendingKeyPair(testEncryptionKey)).privateKey,
expectedHash,
);
},
};
hardwareWallet.setConnector(connector);

const { provedTransactions } = await transactionBatch.generateTransactions(
prover,
hardwareWallet,
txidVersion,
testEncryptionKey,
() => {},
false,
);

expect(provedTransactions).to.have.length(1);
expect(batchApprovalCalls).to.equal(1);
expect(approvedRequestCount).to.equal(1);
expect(expectedHashes).to.have.length(1);
expect(signedSubSessions).to.deep.equal(['batch-sub-session']);
});

it('Should sign hardware wallet transactions without a batch approval sub-session when unsupported', async () => {
transactionBatch.addOutput(await makeNote(1n));

const hardwareWallet = await HardwareWallet.fromShareableViewingKey(
db,
testEncryptionKey,
wallet.generateShareableViewingKey(),
undefined,
prover,
);
await hardwareWallet.loadUTXOMerkletree(txidVersion, utxoMerkletree);
await hardwareWallet.decryptBalances(txidVersion, chain, () => {}, false);
await hardwareWallet.refreshPOIsForTXIDVersion(chain, txidVersion, true);

const signedSubSessions: Optional<string>[] = [];
const connector: ExternalSignerConnector = {
sign: async (expectedHash, _publicInputs, subSession) => {
signedSubSessions.push(subSession);
return signEDDSA(
(await wallet.getSpendingKeyPair(testEncryptionKey)).privateKey,
expectedHash,
);
},
};
hardwareWallet.setConnector(connector);

const { provedTransactions } = await transactionBatch.generateTransactions(
prover,
hardwareWallet,
txidVersion,
testEncryptionKey,
() => {},
false,
);

expect(provedTransactions).to.have.length(1);
expect(signedSubSessions).to.deep.equal([undefined]);
});

it('Should generate validated inputs for transaction batch', async () => {
transactionBatch.addOutput(await makeNote());
const spendingSolutionGroups =
Expand Down
45 changes: 41 additions & 4 deletions src/transaction/transaction-batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import { stringifySafe } from '../utils/stringify';
import { Chain } from '../models/engine-types';
import { TransactNote } from '../note/transact-note';
import {
PrivateInputsRailgun,
PreTransactionPOIsPerTxidLeafPerList,
PublicInputsRailgun,
TXIDVersion,
TreeBalance,
UnprovedTransactionInputs,
} from '../models';
import { getTokenDataHash } from '../note/note-util';
import { AbstractWallet } from '../wallet';
import { AbstractWallet, HardwareWallet } from '../wallet';
import { BoundParamsStruct } from '../abi/typechain/RailgunSmartWallet';
import { isDefined } from '../utils/is-defined';
import { POI } from '../poi';
Expand Down Expand Up @@ -427,9 +429,16 @@ export class TransactionBatch {
data: '0x', // TODO-V3: Add RelayAdapt encoded calldata
};

for (let index = 0; index < transactionDatas.length; index += 1) {
const { transaction, utxos, hasUnshield } = transactionDatas[index];
const generatedRequests: {
transaction: Transaction;
utxos: TXO[];
hasUnshield: boolean;
publicInputs: PublicInputsRailgun;
privateInputs: PrivateInputsRailgun;
boundParams: BoundParamsStruct | PoseidonMerkleVerifier.BoundParamsStruct;
}[] = [];

for (const { transaction, utxos, hasUnshield } of transactionDatas) {
const { publicInputs, privateInputs, boundParams } =
// eslint-disable-next-line no-await-in-loop
await transaction.generateTransactionRequest(
Expand All @@ -439,8 +448,36 @@ export class TransactionBatch {
globalBoundParams,
);

generatedRequests.push({
transaction,
utxos,
hasUnshield,
publicInputs,
privateInputs,
boundParams,
});
}

let subSession: Optional<string>;
if (wallet instanceof HardwareWallet) {
subSession = await wallet.requestBatchApproval(generatedRequests);
}

for (let index = 0; index < generatedRequests.length; index += 1) {
const {
transaction,
utxos,
hasUnshield,
publicInputs,
privateInputs,
boundParams,
} = generatedRequests[index];

// eslint-disable-next-line no-await-in-loop
const signature = await wallet.sign(publicInputs, encryptionKey);
const signature = await wallet.sign(
publicInputs,
wallet instanceof HardwareWallet ? (subSession ?? '') : encryptionKey,
);

// Specific types per TXIDVersion
let treeNumber: BigNumberish;
Expand Down
Loading
Loading