Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multisig: Connect request and SignMultisigTransaction request #458

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8cb07e3
Add test for signing a multisig transaction
sisou Sep 26, 2022
a520a0a
Add `signPartially` method to Key and use in test
sisou Sep 26, 2022
ee6a2ba
MonkeyPatch MerkleTree to verify multisig sender address
sisou Sep 26, 2022
4b00fdf
[WIP] Add SignMultisigTransaction request
sisou Sep 26, 2022
b80eabd
Add demo for multisig signing
sisou Sep 27, 2022
7402611
Add multisig config badge to AddressInfo component
sisou Sep 27, 2022
07da998
Add user and account name section
sisou Sep 29, 2022
a609892
Update title wording
sisou May 22, 2023
f605504
Demo of generating random RSA keys in a sandboxed iframe
sisou Oct 7, 2022
b10662f
Use node-forge in sandboxed iframe to generate deterministic RSA key
sisou Oct 7, 2022
8de308a
Use ArrayBuffers to exchange key material
sisou Oct 10, 2022
35425a4
Extend 32-byte entropy to 1024-byte seed with PBKDF2
sisou Oct 10, 2022
4259030
Refactor Key config into an object and extend with rsaKeyPair option
sisou Oct 11, 2022
ff780c7
Add RSA key computation to Key class
sisou Oct 11, 2022
f90da33
Implement encrypted secret parsing and decryption
sisou Oct 11, 2022
9fb4580
Add Rust code compiled to WASM for secret aggregation
sisou Oct 11, 2022
e424038
Update multisig badge UI, show identicon for legacy accounts
sisou Oct 12, 2022
59aa873
Connect request
sisou Oct 22, 2023
293763a
Extract LoginFileAccountIcon into a component
sisou Oct 18, 2022
36d898e
Refactor multisig request type structure
sisou Oct 19, 2022
7223628
Add comment about permission additions
sisou Oct 21, 2022
0593220
Narrow encryptionKey algorithm type
sisou Oct 21, 2022
63bf825
Add sandboxed iframe files to dist during build
sisou Oct 24, 2022
edb4c2d
Extract inline-script from RSAKeysIframe
sisou Oct 24, 2022
b1ea4a9
Add explainer tooltip to connect UI
sisou May 22, 2023
79a8eae
Clean-up, improve error handling
sisou Oct 22, 2023
d5959d5
Fix types for KeyguardCommand
sisou Dec 1, 2022
7841d8b
Allow RSA key generation to use dynamic parameters
sisou Dec 5, 2022
820d23f
Apply fixes from the Typescript update (PR #467)
sisou Sep 24, 2024
9157108
Allow signing multisig transactions from a vesting contract
sisou Sep 28, 2024
fbbcf06
Update SignMultisigTransaction flow to Albatross PoS SDK
sisou Jan 15, 2025
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.min.*
src/lib/bitcoin/BitcoinJS.js
src/lib/polygon/OpenGSN.js
src/lib/multisig/wasm/pkg
10 changes: 10 additions & 0 deletions client/src/KeyguardClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ReleaseKeyRequest,
ResetPasswordRequest,
SignTransactionRequest,
SignMultisigTransactionRequest,
SignStakingRequest,
SignMessageRequest,
SimpleRequest,
Expand All @@ -40,6 +41,7 @@ import {
SignSwapRequest,
SignSwapTransactionsRequest,
SignSwapTransactionsResult,
ConnectRequest,
} from './PublicRequest';

import Observable from './Observable';
Expand Down Expand Up @@ -121,6 +123,10 @@ export class KeyguardClient {
this._redirectRequest<SignTransactionRequest>(KeyguardCommand.SIGN_TRANSACTION, request);
}

public signMultisigTransaction(request: SignMultisigTransactionRequest) {
this._redirectRequest<SignMultisigTransactionRequest>(KeyguardCommand.SIGN_MULTISIG_TRANSACTION, request);
}

public signStaking(request: SignStakingRequest) {
this._redirectRequest<SignStakingRequest>(KeyguardCommand.SIGN_STAKING, request);
}
Expand Down Expand Up @@ -153,6 +159,10 @@ export class KeyguardClient {
this._redirectRequest<SignSwapRequest>(KeyguardCommand.SIGN_SWAP, request);
}

public connectAccount(request: ConnectRequest) {
this._redirectRequest<ConnectRequest>(KeyguardCommand.CONNECT_ACCOUNT, request);
}

/* IFRAME REQUESTS */

public async list(): Promise<ListResult> {
Expand Down
2 changes: 2 additions & 0 deletions client/src/KeyguardCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ export enum KeyguardCommand {
EXPORT = 'export',
CHANGE_PASSWORD = 'change-password',
SIGN_TRANSACTION = 'sign-transaction',
SIGN_MULTISIG_TRANSACTION = 'sign-multisig-transaction',
SIGN_STAKING = 'sign-staking',
SIGN_MESSAGE = 'sign-message',
CONNECT_ACCOUNT = 'connect',
DERIVE_ADDRESS = 'derive-address',

// Bitcoin
Expand Down
69 changes: 67 additions & 2 deletions client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export type BitcoinTransactionInfo = {
};

export type SignTransactionRequestLayout = 'standard' | 'checkout' | 'cashlink';
export type SignMultisigTransactionRequestLayout = 'standard';
export type SignBtcTransactionRequestLayout = 'standard' | 'checkout';

// Specific Requests
Expand Down Expand Up @@ -189,6 +190,40 @@ export type SignTransactionRequest
| SignTransactionRequestCheckout
| SignTransactionRequestCashlink;

export type EncryptionKeyParams = {
kdf: string,
iterations: number,
keySize: number,
};

export type MultisigConfig = {
publicKeys: Uint8Array[],
signers: Array<{
publicKey: Uint8Array,
commitments: Uint8Array[],
}>,
secrets: Uint8Array[] | {
encrypted: Uint8Array[],
keyParams: EncryptionKeyParams,
},
userName?: string,
};

export type SignMultisigTransactionRequestCommon = Transform<SignTransactionRequestCommon, 'keyLabel' | 'senderLabel', {
keyLabel: string, // Not optional
senderLabel: string, // Not optional
}> & {
multisigConfig: MultisigConfig,
};

export type SignMultisigTransactionRequestStandard = SignMultisigTransactionRequestCommon & {
layout?: 'standard',
recipientLabel?: string,
};

export type SignMultisigTransactionRequest
= SignMultisigTransactionRequestStandard;

export type SignStakingRequest = SimpleRequest & {
keyPath: string,
transaction: Uint8Array | Uint8Array[], // An array is only allowed for retire_stake + remove_stake transactions
Expand Down Expand Up @@ -492,6 +527,24 @@ export type DerivePolygonAddressResult = {
}>,
};

export type ConnectRequest = SimpleRequest & {
appLogoUrl: string,
permissions: KeyguardCommand[],
requestedKeyPaths: string[],
challenge: string,
};

export type ConnectResult = {
signatures: SignatureResult[],
encryptionKey: {
format: 'spki',
keyData: Uint8Array,
algorithm: { name: string, hash: string },
keyUsages: ['encrypt'],
keyParams: EncryptionKeyParams,
},
};

// Request unions

export type RedirectRequest
Expand All @@ -501,10 +554,12 @@ export type RedirectRequest
| ImportRequest
| RemoveKeyRequest
| SignMessageRequest
| ConnectRequest
| SignTransactionRequest
| SignStakingRequest
| SignBtcTransactionRequest
| SignPolygonTransactionRequest
| SignMultisigTransactionRequest
| SimpleRequest
| DeriveBtcXPubRequest
| DerivePolygonAddressRequest
Expand Down Expand Up @@ -545,6 +600,7 @@ export type ListLegacyResult = LegacyKeyInfoObject[];
export type SignTransactionResult = SignatureResult & {
serializedTx: Uint8Array,
};
export type SignMultisigTransactionResult = SignatureResult;
export type SignStakingResult = SignatureResult & {
transaction: Uint8Array,
};
Expand Down Expand Up @@ -579,7 +635,10 @@ export type RedirectResult
= DerivedAddress[]
| ExportResult
| KeyResult
| SignatureResult
| ConnectResult
| SignTransactionResult
| SignMultisigTransactionResult
| SignStakingResult[]
| SignedBitcoinTransaction
| SignedPolygonTransaction
Expand All @@ -593,8 +652,11 @@ export type Result = RedirectResult | IFrameResult;
// Derived Result types

export type ResultType<T extends RedirectRequest> =
T extends Is<T, SignMessageRequest> | Is<T, SignTransactionRequest> ? SignatureResult :
T extends Is<T, SignMessageRequest> ? SignatureResult :
T extends Is<T, SignTransactionRequest> ? SignTransactionResult :
T extends Is<T, SignMultisigTransactionRequest> ? SignMultisigTransactionResult :
T extends Is<T, SignStakingRequest> ? SignStakingResult[] :
T extends Is<T, ConnectRequest> ? ConnectResult :
T extends Is<T, DeriveAddressRequest> ? DerivedAddress[] :
T extends Is<T, CreateRequest> | Is<T, ImportRequest> | Is<T, ResetPasswordRequest> ? KeyResult :
T extends Is<T, ExportRequest> ? ExportResult :
Expand All @@ -607,8 +669,11 @@ export type ResultType<T extends RedirectRequest> =
never;

export type ResultByCommand<T extends KeyguardCommand> =
T extends KeyguardCommand.SIGN_MESSAGE | KeyguardCommand.SIGN_TRANSACTION ? SignatureResult :
T extends KeyguardCommand.SIGN_MESSAGE ? SignatureResult :
T extends KeyguardCommand.SIGN_TRANSACTION ? SignTransactionResult :
T extends KeyguardCommand.SIGN_MULTISIG_TRANSACTION ? SignMultisigTransactionResult :
T extends KeyguardCommand.SIGN_STAKING ? SignStakingResult[] :
T extends KeyguardCommand.CONNECT_ACCOUNT ? ConnectResult :
T extends KeyguardCommand.DERIVE_ADDRESS ? DerivedAddress[] :
T extends KeyguardCommand.CREATE | KeyguardCommand.IMPORT ? KeyResult :
T extends KeyguardCommand.EXPORT ? ExportResult :
Expand Down
119 changes: 119 additions & 0 deletions demos/ConnectAccount.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SignTransaction | Keyguard Demo</title>
<link href="../src/nimiq-style.css" rel="stylesheet">
<script src="../node_modules/@nimiq/core-web/web-offline.js"></script>
<script src="../src/lib/KeyInfo.js"></script>
<script src="../src/lib/KeyStore.js"></script>
<script src="../node_modules/@nimiq/rpc/dist/rpc.umd.js"></script>

<style>
.row {
margin: 1rem 0;
}

label, input {
display: block;
margin: 5px;
}

#result {
max-width: 900px;
overflow-wrap: break-word;
}
</style>
</head>
<body>

<form class="center">
<div class="row">
<label>Account to connect</label>
<select id="account"></select>
</div>

<div class="row">
<label>Challenge</label>
<input id="challenge" value="some random characters">
</div>

<div class="row">
<label>Permissions</label>
<label>
<input type="checkbox" id="permission-sign-multisig-transaction" checked> sign-multisig-transaction
</label>
</div>

<!-- <br><button id="redirect" type="button" class="small" disabled>Connect Account (redirect)</button> -->
<br><button id="popup" type="button" class="small">Connect Account (popup)</button>

<p id="result"></p>
</form>

<script>
const accountSelector = document.querySelector('#account');

async function loadAccounts() {
/** @type {KeyInfo[]} */
const keyInfos = await KeyStore.instance.list();
const dom = document.createDocumentFragment();
for (const keyInfo of keyInfos) {
const option = document.createElement('option');
option.value = keyInfo.id;
option.textContent = keyInfo.defaultAddress.toUserFriendlyAddress();
dom.appendChild(option);
}
accountSelector.appendChild(dom);
}
loadAccounts();

document.querySelector('button#popup').addEventListener('click', async () => {
connectAccountPopup(await generateRequest());
});

async function generateRequest() {
const keyId = accountSelector.value;
const challenge = document.querySelector('#challenge').value;
const permissions = ['sign-multisig-transaction']; // TODO

const request = {
appName: 'Nimiq Multisig',

keyId,
keyLabel: 'Some Account',

appLogoUrl: `${window.location.origin}/demos/multisig-logo.svg`,
permissions,
requestedKeyPaths: [`m/44'/242'/0'/0'`],
challenge,
};

return request;
}

// function connectAccountRedirect(txRequest) {
// return client.connect(txRequest, RedirectRequestBehavior.withLocalState({ keyId: txRequest.keyId }));
// }

async function connectAccountPopup(txRequest) {
const keyguard = window.open('../src/request/connect/', 'ConnectAccount Demo',
`left=${window.innerWidth / 2 - 350},top=75,width=700,height=850,location=yes,dependent=yes`);
const rpc = new Rpc.PostMessageRpcClient(keyguard, '*');
await rpc.init();

try {
const result = await rpc.call('request', txRequest);
console.log('Keyguard result:', result);
document.querySelector('#result').textContent = 'TX signed: ' + JSON.stringify(result);
} catch (e) {
console.error('Keyguard error', e);
document.querySelector('#result').textContent = `Error: ${e.message || e}`;
}

keyguard.close();
}
</script>

</body>
</html>
Loading
Loading