From 7c08fe540904bb0ae2e0b7bd3507b18e5d463b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 22 May 2023 10:39:03 +0200 Subject: [PATCH 01/13] Introduce permissions on WalletInfo --- src/lib/CookieDecoder.ts | 2 ++ src/lib/WalletInfo.ts | 6 +++++- tests/unit/CookieJar.spec.ts | 37 ++++++++++++++++++++++++++---------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/lib/CookieDecoder.ts b/src/lib/CookieDecoder.ts index 41ce92fd..568c3732 100644 --- a/src/lib/CookieDecoder.ts +++ b/src/lib/CookieDecoder.ts @@ -93,6 +93,7 @@ export class CookieDecoder { wordsExported, btcAddresses: { internal: [], external: [] }, polygonAddresses: [], + permissions: {}, }; return walletInfoEntry; @@ -131,6 +132,7 @@ export class CookieDecoder { btcXPub, btcAddresses: { internal: [], external: [] }, polygonAddresses, + permissions: {}, }; return walletInfoEntry; diff --git a/src/lib/WalletInfo.ts b/src/lib/WalletInfo.ts index 2d1c6b4d..5a9bd757 100644 --- a/src/lib/WalletInfo.ts +++ b/src/lib/WalletInfo.ts @@ -40,7 +40,8 @@ export class WalletInfo { ); return new WalletInfo(o.id, o.keyId, o.label, accounts, contracts, o.type, - o.keyMissing, o.fileExported, o.wordsExported, o.btcXPub, btcAddresses, polygonAddresses); + o.keyMissing, o.fileExported, o.wordsExported, o.btcXPub, btcAddresses, polygonAddresses, + o.permissions); } public static async objectToAccountType(o: WalletInfoEntry, requestType: RequestType): Promise { @@ -93,6 +94,7 @@ export class WalletInfo { external: [], }, public polygonAddresses: PolygonAddressInfo[] = [], + public permissions: Record = {}, ) {} public get defaultLabel(): string { @@ -210,6 +212,7 @@ export class WalletInfo { external: this.btcAddresses.external.map((btcAddressInfo) => btcAddressInfo.toObject()), }, polygonAddresses: this.polygonAddresses.map((polygonAddressInfo) => polygonAddressInfo.toObject()), + permissions: this.permissions, }; } @@ -257,4 +260,5 @@ export interface WalletInfoEntry { external: BtcAddressInfoEntry[], }; polygonAddresses?: PolygonAddressEntry[]; + permissions?: Record; } diff --git a/tests/unit/CookieJar.spec.ts b/tests/unit/CookieJar.spec.ts index d485f5a9..7d8d0deb 100644 --- a/tests/unit/CookieJar.spec.ts +++ b/tests/unit/CookieJar.spec.ts @@ -6,6 +6,7 @@ import CookieJar from '@/lib/CookieJar'; import { Utf8Tools } from '@nimiq/utils'; import { setLanguage } from '@/i18n/i18n-setup'; import { BtcAddressInfoEntry } from '@/lib/bitcoin/BtcAddressInfo'; +import { RequestType } from '../../client/PublicRequestTypes'; setup(); @@ -63,6 +64,11 @@ const DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ address: DUMMY_ADDRESS_S1, path: `m/44'/699'/0'/0/0`, // Test that this path is ignored during encoding/decoding }], + permissions: { + 'example.com': [ + RequestType.SIGN_MULTISIG_TRANSACTION, + ], + }, }, { id: '1ee3d926a49d', @@ -93,8 +99,9 @@ const DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ fileExported: true, wordsExported: false, // btcXPub: undefined, - btcAddresses: { internal: [], external: [] }, - polygonAddresses: [], + // btcAddresses: { internal: [], external: [] }, + // polygonAddresses: [], + // permissions: {}, }, { id: '2978bf29b377', @@ -115,8 +122,9 @@ const DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ keyMissing: true, fileExported: false, wordsExported: true, - btcAddresses: { internal: [], external: [] }, - polygonAddresses: [], + // btcAddresses: { internal: [], external: [] }, + // polygonAddresses: [], + // permissions: {}, }, { id: '78bf29b377e7', @@ -146,8 +154,9 @@ const DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ fileExported: true, wordsExported: true, // btcXPub: undefined, - btcAddresses: { internal: [], external: [] }, - polygonAddresses: [], + // btcAddresses: { internal: [], external: [] }, + // polygonAddresses: [], + // permissions: {}, }, { id: 'a5832a3b9489', @@ -170,8 +179,9 @@ const DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ wordsExported: false, btcXPub: 'tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLNGbhd2pq7ZtDiPYTfJ7iBenLVQpYgSQqPjUsQeJX' + 'H8VQ8xA67D', - btcAddresses: { internal: [], external: [] }, - polygonAddresses: [], + // btcAddresses: { internal: [], external: [] }, + // polygonAddresses: [], + // permissions: {}, }, { id: 'd515aa19c4f7', @@ -201,8 +211,9 @@ const DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ keyMissing: false, fileExported: true, wordsExported: false, - btcAddresses: { internal: [], external: [] }, - polygonAddresses: [], + // btcAddresses: { internal: [], external: [] }, + // polygonAddresses: [], + // permissions: {}, }, ]; @@ -241,6 +252,7 @@ const OUT_DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ address: DUMMY_ADDRESS_S1, path: 'not public', }], + permissions: {}, }, { id: '1ee3d926a49d', @@ -273,6 +285,7 @@ const OUT_DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ // btcXPub: undefined, btcAddresses: { internal: [], external: [] }, polygonAddresses: [], + permissions: {}, }, { id: '2978bf29b377', @@ -295,6 +308,7 @@ const OUT_DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ wordsExported: true, btcAddresses: { internal: [], external: [] }, polygonAddresses: [], + permissions: {}, }, { id: '78bf29b377e7', @@ -326,6 +340,7 @@ const OUT_DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ // btcXPub: undefined, btcAddresses: { internal: [], external: [] }, polygonAddresses: [], + permissions: {}, }, { id: 'a5832a3b9489', @@ -350,6 +365,7 @@ const OUT_DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ + 'H8VQ8xA67D', btcAddresses: { internal: [], external: [] }, polygonAddresses: [], + permissions: {}, }, { id: 'd515aa19c4f7', @@ -381,6 +397,7 @@ const OUT_DUMMY_WALLET_OBJECTS: WalletInfoEntry[] = [ wordsExported: false, btcAddresses: { internal: [], external: [] }, polygonAddresses: [], + permissions: {}, }, ]; From 4fbbdd8ac71e18cb3d70206320214a3429d18db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 22 May 2023 12:01:20 +0200 Subject: [PATCH 02/13] Add SignMultisigTransactionRequest public request type --- client/PublicRequestTypes.ts | 36 ++++++++++++++++++++++++++++++++++++ src/lib/RequestTypes.ts | 31 +++++++++++++++++++++++++++++++ src/lib/RpcApi.ts | 5 ++++- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/client/PublicRequestTypes.ts b/client/PublicRequestTypes.ts index 8e885f7f..a62b3ef8 100644 --- a/client/PublicRequestTypes.ts +++ b/client/PublicRequestTypes.ts @@ -16,6 +16,7 @@ export enum RequestType { CHECKOUT = 'checkout', SIGN_MESSAGE = 'sign-message', SIGN_TRANSACTION = 'sign-transaction', + SIGN_MULTISIG_TRANSACTION = 'sign-multisig-transaction', ONBOARD = 'onboard', SIGNUP = 'signup', LOGIN = 'login', @@ -241,6 +242,37 @@ export interface SignedTransaction { }; } +export interface MultisigInfo { + publicKeys: Bytes[]; + numberOfSigners: number; + signerPublicKeys?: Bytes[]; // Can be omitted when all publicKeys need to sign + secret: { + aggregatedSecret: Bytes; + } | { + encryptedSecrets: Bytes[]; + bScalar: Bytes; + }; + aggregatedCommitment: Bytes; + userName?: string; +} + +export interface SignMultisigTransactionRequest extends BasicRequest { + signer: string; // Address + + sender: string; + senderLabel: string; + recipient: string; + recipientType?: Nimiq.Account.Type; + recipientLabel?: string; + value: number; + fee?: number; + extraData?: Bytes; + flags?: number; + validityStartHeight: number; // FIXME To be made optional when hub has its own network + + multisigConfig: MultisigInfo; +} + export interface NimiqHtlcCreationInstructions { type: 'NIM'; sender: string; // My address, must be redeem address of HTLC, or if contract, its owner must be redeem address @@ -446,6 +478,8 @@ export interface SignedMessage { signature: Uint8Array; } +export type PartialSignature = SignedMessage; + export interface Address { address: string; // Userfriendly address label: string; @@ -643,6 +677,7 @@ export interface SignedPolygonTransaction { } export type RpcRequest = SignTransactionRequest + | SignMultisigTransactionRequest | CreateCashlinkRequest | ManageCashlinkRequest | CheckoutRequest @@ -660,6 +695,7 @@ export type RpcRequest = SignTransactionRequest | RefundSwapRequest; export type RpcResult = SignedTransaction + | PartialSignature | Account | Account[] | SimpleResult diff --git a/src/lib/RequestTypes.ts b/src/lib/RequestTypes.ts index 99b6124d..4485c19c 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -53,6 +53,37 @@ export interface ParsedSignTransactionRequest extends ParsedBasicRequest { validityStartHeight: number; // FIXME To be made optional when hub has its own network } +export interface ParsedMultisigInfo { + publicKeys: Uint8Array[]; + numberOfSigners: number; + signerPublicKeys?: Uint8Array[]; // Can be omitted when all publicKeys need to sign + secret: { + aggregatedSecret: Uint8Array; + } | { + encryptedSecrets: Uint8Array[]; + bScalar: Uint8Array; + }; + aggregatedCommitment: Uint8Array; + userName?: string; +} + +export interface ParsedSignMultisigTransactionRequest extends ParsedBasicRequest { + signer: Nimiq.Address; + + sender: Nimiq.Address; + senderLabel: string; + recipient: Nimiq.Address; + recipientType?: Nimiq.Account.Type; + recipientLabel?: string; + value: number; + fee?: number; + extraData?: Uint8Array; + flags?: number; + validityStartHeight: number; // FIXME To be made optional when hub has its own network + + multisigConfig: ParsedMultisigInfo; +} + export type ParsedProtocolSpecificsForCurrency = C extends Currency.NIM ? ParsedNimiqSpecifics : C extends Currency.BTC ? ParsedBitcoinSpecifics diff --git a/src/lib/RpcApi.ts b/src/lib/RpcApi.ts index d9200408..b16bb53b 100644 --- a/src/lib/RpcApi.ts +++ b/src/lib/RpcApi.ts @@ -31,6 +31,7 @@ import { ERROR_CANCELED, WalletType } from './Constants'; import { includesOrigin } from '@/lib/Helpers'; import Config from 'config'; import { setHistoryStorage, getHistoryStorage } from '@/lib/Helpers'; +import { WalletInfo } from './WalletInfo'; export default class RpcApi { private static get HISTORY_KEY_RPC_STATE() { @@ -69,6 +70,7 @@ export default class RpcApi { this._registerHubApis([ RequestType.SIGN_TRANSACTION, + RequestType.SIGN_MULTISIG_TRANSACTION, RequestType.CREATE_CASHLINK, RequestType.MANAGE_CASHLINK, RequestType.CHECKOUT, @@ -93,6 +95,7 @@ export default class RpcApi { ]); this._registerKeyguardApis([ KeyguardCommand.SIGN_TRANSACTION, + KeyguardCommand.SIGN_MULTISIG_TRANSACTION, KeyguardCommand.CREATE, KeyguardCommand.IMPORT, KeyguardCommand.EXPORT, @@ -302,7 +305,7 @@ export default class RpcApi { } } - let account; + let account: WalletInfo | null | undefined; // Simply testing if the property exists (with `'walletId' in request`) is not enough, // as `undefined` also counts as existing. if (request) { From 0ae839931105d2af5502d8d87a03df5c22425b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Wed, 19 Oct 2022 20:19:23 +0200 Subject: [PATCH 03/13] Handle permissioned request types in RpcApi --- src/lib/RpcApi.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/lib/RpcApi.ts b/src/lib/RpcApi.ts index b16bb53b..5122c4ca 100644 --- a/src/lib/RpcApi.ts +++ b/src/lib/RpcApi.ts @@ -10,6 +10,7 @@ import { ParsedSignMessageRequest, ParsedSignTransactionRequest, ParsedSignPolygonTransactionRequest, + ParsedSignMultisigTransactionRequest, } from './RequestTypes'; import { RequestParser } from './RequestParser'; import { Currency, RequestType, RpcRequest, RpcResult } from '../../client/PublicRequestTypes'; @@ -54,6 +55,10 @@ export default class RpcApi { // RequestType.SIGN_POLYGON_TRANSACTION, ]; + private _permissionedRequests: RequestType[] = [ + RequestType.SIGN_MULTISIG_TRANSACTION, + ]; + constructor(store: Store, staticStore: StaticStore, router: Router) { this._store = store; this._staticStore = staticStore; @@ -274,9 +279,10 @@ export default class RpcApi { private async _hubApiHandler(requestType: RequestType, state: RpcState, arg: RpcRequest) { let request: ParsedRpcRequest | undefined; - if ( // Check that a non-whitelisted request comes from a privileged origin + if ( // Check that a non-whitelisted request comes from a privileged origin or is permissioned !this._3rdPartyRequestWhitelist.includes(requestType) && !includesOrigin(Config.privilegedOrigins, state.origin) + && !this._permissionedRequests.includes(requestType) // Permissioned requests are handled below ) { state.reply(ResponseStatus.ERROR, new Error(`${state.origin} is unauthorized to call ${requestType}`)); return; @@ -356,7 +362,25 @@ export default class RpcApi { const parsedSignPolygonTransactionRequest = request as ParsedSignPolygonTransactionRequest; const address = parsedSignPolygonTransactionRequest.request.from; account = this._store.getters.findWalletByPolygonAddress(address); + } else if (this._permissionedRequests.includes(requestType)) { + accountRequired = true; + + if (requestType === RequestType.SIGN_MULTISIG_TRANSACTION) { + const address = (request as ParsedSignMultisigTransactionRequest).signer; + if (address) { + account = this._store.getters.findWalletByAddress(address.toUserFriendlyAddress(), false); + } + } + + if (account && !( + account.permissions[state.origin] + && account.permissions[state.origin].includes(requestType) + )) { + this.reject(new Error('Method not allowed - requires permission')); + return; + } } + if (accountRequired && !account) { this.reject(new Error(errorMsg)); return; From 9898eb6923470c3d775729173304f8f9f5dd60fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Wed, 19 Oct 2022 14:52:57 +0200 Subject: [PATCH 04/13] Parse SignMultisigTransactionRequest --- src/lib/RequestParser.ts | 91 ++++++++++++++++++++++++++++++++++++++++ src/lib/RequestTypes.ts | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/lib/RequestParser.ts b/src/lib/RequestParser.ts index c5ea0577..404875e6 100644 --- a/src/lib/RequestParser.ts +++ b/src/lib/RequestParser.ts @@ -23,6 +23,7 @@ import { SignPolygonTransactionRequest, SetupSwapRequest, RefundSwapRequest, + SignMultisigTransactionRequest, } from '../../client/PublicRequestTypes'; import type { ParsedBasicRequest, @@ -41,6 +42,7 @@ import type { ParsedSignPolygonTransactionRequest, ParsedSetupSwapRequest, ParsedRefundSwapRequest, + ParsedSignMultisigTransactionRequest, } from './RequestTypes'; import { ParsedNimiqDirectPaymentOptions } from './paymentOptions/NimiqPaymentOptions'; import { ParsedEtherDirectPaymentOptions } from './paymentOptions/EtherPaymentOptions'; @@ -690,6 +692,74 @@ export class RequestParser { }, }; return parsedRefundSwapRequest; + case RequestType.SIGN_MULTISIG_TRANSACTION: + const signMultisigTxRequest = request as SignMultisigTransactionRequest; + + // Most fields are validated by Keyguard, we mustly need to do type conversions if necessary + + // TODO: Validate object and array fields, to not throw "tried to access of undefined" errors + + // if ( + // !signMultisigTxRequest.multisigConfig || + // typeof signMultisigTxRequest.multisigConfig !== 'object' + // ) { + // throw new Error('multisigConfig must be an object'); + // } + + // if ( + // !signMultisigTxRequest.multisigConfig.secret + // || typeof signMultisigTxRequest.multisigConfig.secret !== 'object' + // ) { + // throw new Error('multisigConfig.secret must be an object'); + // } + + function parseBytes(bytes: Uint8Array | string | undefined, allowEmpty: true): Uint8Array | undefined; + function parseBytes(bytes: Uint8Array | string): Uint8Array; + function parseBytes(bytes?: Uint8Array | string, allowEmpty?: boolean) { + if (!bytes && !allowEmpty) { + throw new Error('bytes cannot be empty'); + } + return typeof bytes === 'string' ? Utf8Tools.stringToUtf8ByteArray(bytes) : bytes; + } + + const parsedSignMultisigTxRequest: ParsedSignMultisigTransactionRequest = { + kind: RequestType.SIGN_TRANSACTION, + appName: signMultisigTxRequest.appName, + + signer: Nimiq.Address.fromAny(signMultisigTxRequest.signer), + + sender: Nimiq.Address.fromString(signMultisigTxRequest.sender), + senderLabel: signMultisigTxRequest.senderLabel, + recipient: Nimiq.Address.fromString(signMultisigTxRequest.recipient), + recipientType: signMultisigTxRequest.recipientType || Nimiq.Account.Type.BASIC, + recipientLabel: signMultisigTxRequest.recipientLabel, + value: signMultisigTxRequest.value, + fee: signMultisigTxRequest.fee || 0, + data: parseBytes(signMultisigTxRequest.extraData, true) || new Uint8Array(0), + flags: signMultisigTxRequest.flags || Nimiq.Transaction.Flag.NONE, + validityStartHeight: signMultisigTxRequest.validityStartHeight, + + multisigConfig: { + publicKeys: signMultisigTxRequest.multisigConfig.publicKeys.map(parseBytes), + numberOfSigners: signMultisigTxRequest.multisigConfig.numberOfSigners, + signerPublicKeys: signMultisigTxRequest.multisigConfig.signerPublicKeys + ? signMultisigTxRequest.multisigConfig.signerPublicKeys.map(parseBytes) + : signMultisigTxRequest.multisigConfig.publicKeys.map(parseBytes), + secret: 'aggregatedSecret' in signMultisigTxRequest.multisigConfig.secret + ? {aggregatedSecret: parseBytes( + signMultisigTxRequest.multisigConfig.secret.aggregatedSecret, + ) } + : { + encryptedSecrets: signMultisigTxRequest.multisigConfig.secret.encryptedSecrets.map( + parseBytes, + ), + bScalar: parseBytes(signMultisigTxRequest.multisigConfig.secret.bScalar), + }, + aggregatedCommitment: parseBytes(signMultisigTxRequest.multisigConfig.aggregatedCommitment), + }, + }; + + return parsedSignMultisigTxRequest; default: return null; } @@ -866,6 +936,27 @@ export class RequestParser { recipient: refundSwapRequest.refund.recipient.toUserFriendlyAddress(), } : refundSwapRequest.refund, } as RefundSwapRequest; + case RequestType.SIGN_MULTISIG_TRANSACTION: + const signMultisigTxRequest = request as ParsedSignMultisigTransactionRequest; + const rawSignMultisigTxRequest: SignMultisigTransactionRequest = { + appName: signMultisigTxRequest.appName, + + signer: signMultisigTxRequest.signer.toUserFriendlyAddress(), + + sender: signMultisigTxRequest.sender.toUserFriendlyAddress(), + senderLabel: signMultisigTxRequest.senderLabel, + recipient: signMultisigTxRequest.recipient.toUserFriendlyAddress(), + recipientType: signMultisigTxRequest.recipientType, + recipientLabel: signMultisigTxRequest.recipientLabel, + value: signMultisigTxRequest.value, + fee: signMultisigTxRequest.fee, + extraData: signMultisigTxRequest.data, + flags: signMultisigTxRequest.flags, + validityStartHeight: signMultisigTxRequest.validityStartHeight, + + multisigConfig: signMultisigTxRequest.multisigConfig, + }; + return rawSignMultisigTxRequest; default: return null; } diff --git a/src/lib/RequestTypes.ts b/src/lib/RequestTypes.ts index 4485c19c..00e7a52c 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -77,7 +77,7 @@ export interface ParsedSignMultisigTransactionRequest extends ParsedBasicRequest recipientLabel?: string; value: number; fee?: number; - extraData?: Uint8Array; + data?: Uint8Array; flags?: number; validityStartHeight: number; // FIXME To be made optional when hub has its own network From 46464f3e80c6d4d91ca7a0a0baf7857323f5712b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Wed, 19 Oct 2022 16:40:51 +0200 Subject: [PATCH 05/13] Fix types in Demo.ts --- demos/Demo.ts | 234 ++++++++++++++++++++++++++------------------------ 1 file changed, 121 insertions(+), 113 deletions(-) diff --git a/demos/Demo.ts b/demos/Demo.ts index 85a0dd42..e3a80b6b 100644 --- a/demos/Demo.ts +++ b/demos/Demo.ts @@ -55,17 +55,17 @@ class Demo { console.log('Hub result', result); console.log('State', state); - document.querySelector('#result').textContent = JSON.stringify(result); + document.querySelector('#result')!.textContent = JSON.stringify(result); }, (error: Error, state: State) => { console.error('Hub error', error); console.log('State', state); - document.querySelector('#result').textContent = `Error: ${error.message || error}`; + document.querySelector('#result')!.textContent = `Error: ${error.message || error}`; }); }); demo.client.checkRedirectResponse(); - document.querySelectorAll('input[name="popup-vs-redirect"]').forEach((input: HTMLInputElement) => { + document.querySelectorAll('input[name="popup-vs-redirect"]').forEach((input) => { input.addEventListener('change', (event) => { const value = (event.target as HTMLInputElement).value; demo.setClientBehavior(value); @@ -74,11 +74,11 @@ class Demo { demo.setClientBehavior( (document.querySelector('input[name="popup-vs-redirect"]:checked') as HTMLInputElement).value); - document.querySelector('button#checkout').addEventListener('click', async () => { + document.querySelector('button#checkout')!.addEventListener('click', async () => { await checkout(await generateCheckoutRequest()); }); - document.querySelector('button#multi-checkout').addEventListener('click', async () => { + document.querySelector('button#multi-checkout')!.addEventListener('click', async () => { await checkout(await generateCheckoutRequest(/* multiCheckout */ true)); }); @@ -98,7 +98,7 @@ class Demo { themeSelector.add(option); }); - document.querySelector('button#create-cashlink').addEventListener('click', async () => { + document.querySelector('button#create-cashlink')!.addEventListener('click', async () => { try { let value: number | undefined = parseInt((document.querySelector('#cashlink-value') as HTMLInputElement).value); @@ -150,39 +150,39 @@ class Demo { const result = await demo.client.createCashlink(request, demo._defaultBehavior as PopupRequestBehavior); console.log('Result', result); - document.querySelector('#result').textContent = `Cashlink created${result.link + document.querySelector('#result')!.textContent = `Cashlink created${result.link ? `: ${result.link}` : '' }`; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#choose-address').addEventListener('click', async () => { + document.querySelector('button#choose-address')!.addEventListener('click', async () => { try { const result = await demo.client.chooseAddress({ appName: 'Hub Demos', ui: 2 }, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = `Address was chosen: ${result ? result.address : '-'}`; + document.querySelector('#result')!.textContent = `Address was chosen: ${result ? result.address : '-'}`; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#choose-address-and-btc').addEventListener('click', async () => { + document.querySelector('button#choose-address-and-btc')!.addEventListener('click', async () => { try { const result = await demo.client.chooseAddress({ appName: 'Hub Demos', returnBtcAddress: true, ui: 2 }, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = `Address was chosen: ${result ? result.address : '-'}`; + document.querySelector('#result')!.textContent = `Address was chosen: ${result ? result.address : '-'}`; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#sign-transaction').addEventListener('click', async () => { + document.querySelector('button#sign-transaction')!.addEventListener('click', async () => { const txRequest = generateSignTransactionRequest(); try { const result = await demo.client.signTransaction( @@ -192,43 +192,43 @@ class Demo { demo._defaultBehavior, ); console.log('Result', result); - document.querySelector('#result').textContent = 'TX signed'; + document.querySelector('#result')!.textContent = 'TX signed'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#onboard').addEventListener('click', async () => { + document.querySelector('button#onboard')!.addEventListener('click', async () => { try { const result = await demo.client.onboard({ appName: 'Hub Demos' }, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'Onboarding completed!'; + document.querySelector('#result')!.textContent = 'Onboarding completed!'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#create').addEventListener('click', async () => { + document.querySelector('button#create')!.addEventListener('click', async () => { try { const result = await demo.client.signup({ appName: 'Hub Demos' }, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'New account & address created'; + document.querySelector('#result')!.textContent = 'New account & address created'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#login').addEventListener('click', async () => { + document.querySelector('button#login')!.addEventListener('click', async () => { try { const result = await demo.client.login({ appName: 'Hub Demos' }, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'Account imported'; + document.querySelector('#result')!.textContent = 'Account imported'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); @@ -238,7 +238,7 @@ class Demo { alert('You have no account to send a tx from, create an account first (signup)'); throw new Error('No account found'); } - const sender = ($radio as HTMLElement).dataset.address; + const sender = ($radio as HTMLElement).dataset.address!; const value = parseInt((document.querySelector('#value') as HTMLInputElement).value, 10) || 1337; const fee = parseInt((document.querySelector('#fee') as HTMLInputElement).value, 10) || 0; const txData = (document.querySelector('#data') as HTMLInputElement).value || ''; @@ -346,30 +346,30 @@ class Demo { try { const result = await demo.client.checkout(txRequest, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'TX signed'; + document.querySelector('#result')!.textContent = 'TX signed'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } } - document.querySelector('button#sign-message').addEventListener('click', async () => { + document.querySelector('button#sign-message')!.addEventListener('click', async () => { const request: SignMessageRequest = { appName: 'Hub Demos', // signer: 'NQ63 U7XG 1YYE D6FA SXGG 3F5H X403 NBKN JLDU', - message: (document.querySelector('#message') as HTMLInputElement).value || undefined, + message: (document.querySelector('#message') as HTMLInputElement).value || '', }; try { const result = await demo.client.signMessage(request, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'MSG signed: ' + request.message; + document.querySelector('#result')!.textContent = 'MSG signed: ' + request.message; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#sign-message-with-account').addEventListener('click', async () => { + document.querySelector('button#sign-message-with-account')!.addEventListener('click', async () => { const $radio = document.querySelector('input[name="address"]:checked'); if (!$radio) { alert('You have no account to sign a message by, create an account first (signup)'); @@ -380,20 +380,20 @@ class Demo { const request: SignMessageRequest = { appName: 'Hub Demos', signer, - message: (document.querySelector('#message') as HTMLInputElement).value || undefined, + message: (document.querySelector('#message') as HTMLInputElement).value || '', }; try { const result = await demo.client.signMessage(request, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'MSG signed: ' + request.message; + document.querySelector('#result')!.textContent = 'MSG signed: ' + request.message; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#sign-message-with-tabs').addEventListener('click', async () => { + document.querySelector('button#sign-message-with-tabs')!.addEventListener('click', async () => { const $radio = document.querySelector('input[name="address"]:checked'); const signer = $radio && ($radio as HTMLElement).dataset.address || undefined; @@ -406,21 +406,21 @@ class Demo { try { const result = await demo.client.signMessage(request, demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'MSG signed: ' + request.message; + document.querySelector('#result')!.textContent = 'MSG signed: ' + request.message; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#migrate').addEventListener('click', async () => { + document.querySelector('button#migrate')!.addEventListener('click', async () => { try { const result = await demo.client.migrate(demo._defaultBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'Migrated'; + document.querySelector('#result')!.textContent = 'Migrated'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); @@ -459,7 +459,7 @@ class Demo { } }); - document.querySelector('button#sign-btc-transaction').addEventListener('click', async () => { + document.querySelector('button#sign-btc-transaction')!.addEventListener('click', async () => { const $radio = document.querySelector('input[name="address"]:checked'); if (!$radio) { alert('You have no account to send a tx from, create an account first (signup)'); @@ -503,30 +503,29 @@ class Demo { try { const result = await demo.client.signBtcTransaction(txRequest, demo._defaultBehavior as PopupRequestBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'Signed: ' + result.serializedTx; + document.querySelector('#result')!.textContent = 'Signed: ' + result.serializedTx; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#setup-swap.nim-to-btc').addEventListener('click', async () => { - // const $radio = document.querySelector('input[name="address"]:checked'); - // if (!$radio) { - // alert('You have no account to send a tx from, create an account first (signup)'); - // throw new Error('No account found'); - // } - // const accountId = $radio.closest('ul').closest('li').querySelector('button').dataset.walletId; - const accountId = '44012bb58ff5'; + document.querySelector('button#setup-swap.nim-to-btc')!.addEventListener('click', async () => { + const $radio = document.querySelector('input[name="address"]:checked'); + if (!$radio) { + alert('You have no account to swap with, create an account first (signup)'); + throw new Error('No account found'); + } + const accountId = $radio.closest('ul')!.closest('li')!.querySelector('button')!.dataset.walletId!; const account = (await demo.list()).find((wallet) => wallet.accountId === accountId); if (!account) { - alert('Account for the demo swap not found. Currently only Sören has this account.'); + alert('Account not found.'); throw new Error('Account not found'); } if (account.type === WalletType.LEGACY) { - alert('Cannot sign BTC transactions with a legacy account'); + alert('Cannot swap with a legacy account'); throw new Error('Cannot use legacy account'); } @@ -538,21 +537,18 @@ class Demo { const request: SetupSwapRequest = { appName: 'Hub Demos', + accountId, + swapId: 'example-swap-id', fund: { type: 'NIM', sender: account.addresses[0].address, value: 2709.79904 * 1e5, fee: 0, - extraData: 'anlssPDlYuJ5R8hvRtmP3EVjywhona4vd7BI3MCOFNcxBOoUIitb4QMZNYm9TPJr6LpTyq2WJSLYwtBr6jaor6LrJjgvNFcr4gEAEWWF', validityStartHeight: 1140000, }, redeem: { type: 'BTC', input: { - transactionHash: 'ef4aaf6087d0cc48ff09355d715c257078467ca4d9dd75a20824e70a78fb43cc', - outputIndex: 0, - outputScript: BitcoinJS.address.toOutputScript('tb1q0hzaqgespv4a67wrc843gkjd5s668l6arm820utp32m9nss90ejq83klw7', BitcoinJS.networks.testnet).toString('hex'), - witnessScript: '6382012088a820193589bd4cf26be8ba53caad962522d8c2d06bea36a8afa2eb26382f34572be28876a91484eb9bcbd90ce7d3360992259e4b9b818215a96088ac67044934565fb17576a91457f4babc23d2369572394cf80f28daeb9c3b58f188ac68', value: Math.round(0.001004 * 1e8), }, output: { @@ -562,10 +558,17 @@ class Demo { }, fiatCurrency: 'eur', - nimFiatRate: 0.00267, - btcFiatRate: 8662.93, - serviceNetworkFee: 10.73171 * 1e5, - serviceExchangeFee: 5.40878 * 1e5, + fundingFiatRate: 0.00267, + redeemingFiatRate: 8662.93, + fundFees: { + redeeming: 10.73171 * 1e5, + processing: 0, + }, + redeemFees: { + funding: 0, + processing: 0, + }, + serviceSwapFee: 5.40878 * 1e5, nimiqAddresses: account.addresses.map((address) => ({ address: address.address, balance: Math.round(Math.random() * 10000 + 3000) * 1e5, @@ -577,30 +580,29 @@ class Demo { try { const result = await demo.client.setupSwap(request, demo._defaultBehavior as PopupRequestBehavior); console.log('Result', result); - document.querySelector('#result').innerHTML = `Signed successfully!
NIM: ${result.nim.serializedTx}
BTC: ${result.btc.serializedTx}`; + document.querySelector('#result')!.innerHTML = `Signed successfully!
NIM: ${result.nim!.serializedTx}
BTC: ${result.btc!.serializedTx}`; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); - document.querySelector('button#setup-swap.btc-to-nim').addEventListener('click', async () => { - // const $radio = document.querySelector('input[name="address"]:checked'); - // if (!$radio) { - // alert('You have no account to send a tx from, create an account first (signup)'); - // throw new Error('No account found'); - // } - // const accountId = $radio.closest('ul').closest('li').querySelector('button').dataset.walletId; - const accountId = '44012bb58ff5'; + document.querySelector('button#setup-swap.btc-to-nim')!.addEventListener('click', async () => { + const $radio = document.querySelector('input[name="address"]:checked'); + if (!$radio) { + alert('You have no account to swap with, create an account first (signup)'); + throw new Error('No account found'); + } + const accountId = $radio.closest('ul')!.closest('li')!.querySelector('button')!.dataset.walletId!; const account = (await demo.list()).find((wallet) => wallet.accountId === accountId); if (!account) { - alert('Account for the demo swap not found. Currently only Sören has this account.'); + alert('Account not found.'); throw new Error('Account not found'); } if (account.type === WalletType.LEGACY) { - alert('Cannot sign BTC transactions with a legacy account'); + alert('Cannot swap with a legacy account'); throw new Error('Cannot use legacy account'); } @@ -612,6 +614,8 @@ class Demo { const request: SetupSwapRequest = { appName: 'Hub Demos', + accountId, + swapId: 'example-swap-id', fund: { type: 'BTC', inputs: [{ @@ -622,27 +626,30 @@ class Demo { value: 0.00076136 * 1e8, }], output: { - address: 'tb1qkg69q2pmq8yncjusk2h77vru99rk8n6pcxdxzzzseupaqc2x64ts4uhrj8', value: 0.00075736 * 1e8, }, refundAddress: refundAddress, - htlcScript: '6382012088a8204b268b25df99a2edb5d9fb59d4ad56402f429a47c751069918a9790743c16b788876a9146ec1c15aa31a3fe4da55ed81fc264a56bae75c7888ac6704cb53565fb17576a91484eb9bcbd90ce7d3360992259e4b9b818215a96088ac68', }, redeem: { type: 'NIM', - sender: 'NQ32 71G4 AQ88 RVA4 4XYC CH39 V2AG HTAM S0YL', recipient: account.addresses[0].address, value: 2000 * 1e5, fee: 0, validityStartHeight: 1140135, - htlcData: 'aJ2uL3ewSNzAjhTXMQTqFCIrW+FqeWyw8OVi4nlHyG9G2Y/cRWPLCANLJosl35mi7bXZ+1nUrVZAL0KaR8dRBpkYqXkHQ8FreAEAEWYf', }, fiatCurrency: 'eur', - nimFiatRate: 0.00267, - btcFiatRate: 8662.93, - serviceNetworkFee: 0.000004 * 1e8, - serviceExchangeFee: Math.round(0.00000151168 * 1e8), + redeemingFiatRate: 0.00267, + fundingFiatRate: 8662.93, + fundFees: { + redeeming: 0.000004 * 1e8, + processing: 0, + }, + redeemFees: { + funding: 0, + processing: 0, + }, + serviceSwapFee: Math.round(0.00000151168 * 1e8), nimiqAddresses: account.addresses.map((address) => ({ address: address.address, balance: Math.round(Math.random() * 5000) * 1e5, @@ -654,10 +661,10 @@ class Demo { try { const result = await demo.client.setupSwap(request, demo._defaultBehavior as PopupRequestBehavior); console.log('Result', result); - document.querySelector('#result').innerHTML = `Signed successfully!
NIM: ${result.nim.serializedTx}
BTC: ${result.btc.serializedTx}`; + document.querySelector('#result')!.innerHTML = `Signed successfully!
NIM: ${result.nim!.serializedTx}
BTC: ${result.btc!.serializedTx}`; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } }); @@ -698,7 +705,7 @@ class Demo { document.querySelector('button#list-keyguard-keys').addEventListener('click', () => demo.listKeyguard()); document.querySelector('button#setup-legacy-accounts').addEventListener('click', () => demo.setupLegacyAccounts()); - document.querySelector('button#list-accounts').addEventListener('click', async () => demo.updateAccounts()); + document.querySelector('button#list-accounts')!.addEventListener('click', async () => demo.updateAccounts()); document.querySelectorAll('button').forEach((button) => button.disabled = false); (document.querySelector('button#list-accounts') as HTMLButtonElement).click(); @@ -733,10 +740,10 @@ class Demo { this._defaultBehavior, ); console.log('Result', result); - document.querySelector('#result').textContent = 'Successfully changed Password'; + document.querySelector('#result')!.textContent = 'Successfully changed Password'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } } @@ -754,10 +761,10 @@ class Demo { this._defaultBehavior, ); console.log('Result', result); - document.querySelector('#result').textContent = 'Account added'; + document.querySelector('#result')!.textContent = 'Account added'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } } @@ -775,10 +782,10 @@ class Demo { this._defaultBehavior, ); console.log('Result', result); - document.querySelector('#result').textContent = 'Done renaming account'; + document.querySelector('#result')!.textContent = 'Done renaming account'; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } } @@ -792,7 +799,7 @@ class Demo { public async updateAccounts() { const cashlinks = await this.cashlinks(); - let $ul = document.querySelector('#cashlinks'); + let $ul = document.querySelector('#cashlinks')!; let cashlinksHtml = ''; cashlinks.forEach((cashlink) => { @@ -814,7 +821,7 @@ class Demo { const wallets = await this.list(); console.log('Accounts in Manager:', wallets); - $ul = document.querySelector('#accounts'); + $ul = document.querySelector('#accounts')!; let html = ''; wallets.forEach((wallet) => { @@ -823,7 +830,7 @@ class Demo { - ${wallet.type !== 0 + ${wallet.type !== WalletType.LEGACY ? `` : ''} @@ -869,31 +876,31 @@ class Demo { (document.querySelector('input[name="address"]') as HTMLInputElement).checked = true; } document.querySelectorAll('button.export').forEach((element) => { - element.addEventListener('click', async () => this.export((element as HTMLElement).dataset.walletId)); + element.addEventListener('click', async () => this.export((element as HTMLElement).dataset.walletId!)); }); document.querySelectorAll('button.export-file').forEach((element) => { - element.addEventListener('click', async () => this.exportFile((element as HTMLElement).dataset.walletId)); + element.addEventListener('click', async () => this.exportFile((element as HTMLElement).dataset.walletId!)); }); document.querySelectorAll('button.export-words').forEach((element) => { - element.addEventListener('click', async () => this.exportWords((element as HTMLElement).dataset.walletId)); + element.addEventListener('click', async () => this.exportWords((element as HTMLElement).dataset.walletId!)); }); document.querySelectorAll('button.change-password').forEach((element) => { element.addEventListener('click', - async () => this.changePassword((element as HTMLElement).dataset.walletId)); + async () => this.changePassword((element as HTMLElement).dataset.walletId!)); }); document.querySelectorAll('button.rename').forEach((element) => { element.addEventListener('click', async () => this.rename( - (element as HTMLElement).dataset.walletId, - (element as HTMLElement).dataset.address, + (element as HTMLElement).dataset.walletId!, + (element as HTMLElement).dataset.address!, ), ); }); document.querySelectorAll('button.add-account').forEach((element) => { - element.addEventListener('click', async () => this.addAccount((element as HTMLElement).dataset.walletId)); + element.addEventListener('click', async () => this.addAccount((element as HTMLElement).dataset.walletId!)); }); document.querySelectorAll('button.logout').forEach((element) => { - element.addEventListener('click', async () => this.logout((element as HTMLElement).dataset.walletId)); + element.addEventListener('click', async () => this.logout((element as HTMLElement).dataset.walletId!)); }); } @@ -915,7 +922,7 @@ class Demo { } public async startPopupClient(url: string, windowName: string): Promise { - const $popup = window.open(url, windowName); + const $popup = window.open(url, windowName)!; const popupClient = new PostMessageRpcClient($popup, '*'); await popupClient.init(); return popupClient; @@ -925,7 +932,7 @@ class Demo { const client = await this.startIframeClient(this._keyguardBaseUrl); const keys = await client.call('list'); console.log('Keys in Keyguard:', keys); - document.querySelector('#result').textContent = 'Keys listed in console'; + document.querySelector('#result')!.textContent = 'Keys listed in console'; return keys; } @@ -938,7 +945,7 @@ class Demo { // @ts-ignore Property '_target' is private and only accessible within class 'PostMessageRpcClient'. client._target.close(); console.log('Legacy Account setup:', result); - document.querySelector('#result').textContent = 'Legacy account stored'; + document.querySelector('#result')!.textContent = 'Legacy account stored'; } public async list(): Promise { @@ -953,11 +960,12 @@ class Demo { try { const result = await this.client.logout(this._createLogoutRequest(accountId), this._defaultBehavior as PopupRequestBehavior); console.log('Result', result); - document.querySelector('#result').textContent = 'Account removed'; + document.querySelector('#result')!.textContent = 'Account removed'; return result; } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; + throw e; } } @@ -996,17 +1004,17 @@ class Demo { const result = await this.client.export(request, this._defaultBehavior as PopupRequestBehavior); console.log('Result', result); if (result.fileExported) { - document.querySelector('#result').textContent = result.wordsExported + document.querySelector('#result')!.textContent = result.wordsExported ? 'Export sucessful' : 'File exported'; } else { - document.querySelector('#result').textContent = result.wordsExported + document.querySelector('#result')!.textContent = result.wordsExported ? 'Words exported' : 'nothing exported'; } } catch (e) { console.error(e); - document.querySelector('#result').textContent = `Error: ${e.message || e}`; + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; } } From e22f0a4ae41a6c4a1c932df5099806401cf9a298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 22 May 2023 10:38:37 +0200 Subject: [PATCH 06/13] Add SignMultisigTransaction views and HubApi method --- client/HubApi.ts | 9 +++ src/i18n/en.po | 4 ++ src/lib/RequestTypes.ts | 2 +- src/router.ts | 15 +++++ src/views/SignMessageSuccess.vue | 2 - src/views/SignMultisigTransaction.vue | 68 ++++++++++++++++++++ src/views/SignMultisigTransactionSuccess.vue | 34 ++++++++++ 7 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/views/SignMultisigTransaction.vue create mode 100644 src/views/SignMultisigTransactionSuccess.vue diff --git a/client/HubApi.ts b/client/HubApi.ts index 6e53b3e9..59d13da8 100644 --- a/client/HubApi.ts +++ b/client/HubApi.ts @@ -43,6 +43,8 @@ import { SetupSwapRequest, SetupSwapResult, RefundSwapRequest, + SignMultisigTransactionRequest, + PartialSignature, } from './PublicRequestTypes'; export default class HubApi< @@ -207,6 +209,13 @@ export default class HubApi< return this._request(requestBehavior, RequestType.REFUND_SWAP, [request]); } + public signMultisigTransaction( + request: Promise | SignMultisigTransactionRequest, + requestBehavior: RequestBehavior = this._defaultBehavior as any, + ): Promise { + return this._request(requestBehavior, RequestType.SIGN_MULTISIG_TRANSACTION, [request]); + } + /** * Account Management * diff --git a/src/i18n/en.po b/src/i18n/en.po index a5a09d33..69ea9611 100644 --- a/src/i18n/en.po +++ b/src/i18n/en.po @@ -1126,6 +1126,10 @@ msgstr "" msgid "Total fees" msgstr "" +#: src/views/SignMultisigTransactionSuccess.vue:4 +msgid "Transaction approved" +msgstr "" + #: src/views/CashlinkManage.vue:211 #: src/views/CheckoutTransmission.vue:61 msgid "Transaction could not be relayed" diff --git a/src/lib/RequestTypes.ts b/src/lib/RequestTypes.ts index 00e7a52c..90c50b4b 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -56,7 +56,7 @@ export interface ParsedSignTransactionRequest extends ParsedBasicRequest { export interface ParsedMultisigInfo { publicKeys: Uint8Array[]; numberOfSigners: number; - signerPublicKeys?: Uint8Array[]; // Can be omitted when all publicKeys need to sign + signerPublicKeys: Uint8Array[]; // Can be omitted when all publicKeys need to sign secret: { aggregatedSecret: Uint8Array; } | { diff --git a/src/router.ts b/src/router.ts index d6f56cf2..190fcf39 100644 --- a/src/router.ts +++ b/src/router.ts @@ -91,6 +91,11 @@ const SetupSwapLedger = () => import(/*webpackChunkName: "swap-ledger"*/ const RefundSwapLedger = () => import(/*webpackChunkName: "refund-swap-ledger"*/ './views/RefundSwapLedger.vue'); +const SignMultisigTransaction = () => import(/*webpackChunkName: "sign-multisig-transaction"*/ + './views/SignMultisigTransaction.vue'); +const SignMultisigTransactionSuccess = () => import(/*webpackChunkName: "sign-multisig-transaction"*/ + './views/SignMultisigTransactionSuccess.vue'); + Vue.use(Router); export function keyguardResponseRouter( @@ -402,5 +407,15 @@ export default new Router({ component: RefundSwapLedger, name: `${RequestType.REFUND_SWAP}-ledger`, }, + { + path: `/${RequestType.SIGN_MULTISIG_TRANSACTION}`, + component: SignMultisigTransaction, + name: RequestType.SIGN_MULTISIG_TRANSACTION, + }, + { + path: `/${RequestType.SIGN_MULTISIG_TRANSACTION}/success`, + component: SignMultisigTransactionSuccess, + name: `${RequestType.SIGN_MULTISIG_TRANSACTION}-success`, + }, ], }); diff --git a/src/views/SignMessageSuccess.vue b/src/views/SignMessageSuccess.vue index fb2296e4..3f9b1b6b 100644 --- a/src/views/SignMessageSuccess.vue +++ b/src/views/SignMessageSuccess.vue @@ -9,7 +9,6 @@ diff --git a/src/views/SignMultisigTransactionSuccess.vue b/src/views/SignMultisigTransactionSuccess.vue new file mode 100644 index 00000000..f32ad52c --- /dev/null +++ b/src/views/SignMultisigTransactionSuccess.vue @@ -0,0 +1,34 @@ + + + From c99b7f0f35cacff4c98ec6850d55a0d1772501f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Wed, 19 Oct 2022 20:19:33 +0200 Subject: [PATCH 07/13] Add SignMultisigTransaction demo --- demos/Demo.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++++-- demos/index.html | 33 +++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/demos/Demo.ts b/demos/Demo.ts index e3a80b6b..f435bf6a 100644 --- a/demos/Demo.ts +++ b/demos/Demo.ts @@ -16,11 +16,13 @@ import { CashlinkTheme, RequestType, SetupSwapRequest, + SignMultisigTransactionRequest, } from '../client/PublicRequestTypes'; import { RedirectRequestBehavior, PopupRequestBehavior } from '../client/RequestBehavior'; import { Utf8Tools } from '@nimiq/utils'; import { WalletType } from '../src/lib/Constants'; import { WalletStore } from '../src/lib/WalletStore'; +import { loadNimiq } from '../src/lib/Helpers'; // BitcoinJS is defined as a global variable in BitcoinJS.min.js loaded by demos/index.html declare global { @@ -702,8 +704,84 @@ class Demo { } }); - document.querySelector('button#list-keyguard-keys').addEventListener('click', () => demo.listKeyguard()); - document.querySelector('button#setup-legacy-accounts').addEventListener('click', + document.querySelector('button#sign-multisig-transaction')!.addEventListener('click', async () => { + const $radio = document.querySelector('input[name="address"]:checked'); + if (!$radio) { + alert('You have no account to sign with, create an account first (signup)'); + throw new Error('No account found'); + } + const accountId = $radio.closest('ul')!.closest('li')!.querySelector('button')!.dataset.walletId!; + const account = (await demo.list()).find((wallet) => wallet.accountId === accountId)!; + + const publicKey = (document.querySelector('#multisig-publickey') as HTMLInputElement).value; + if (publicKey.length !== 64) { + alert('Invalid public key. Enter your public key in HEX format'); + throw new Error('Invalid public key'); + } + + const numberOfSigners = parseInt((document.querySelector('#multisig-signers') as HTMLInputElement).value); + const numberOfKeys = parseInt((document.querySelector('#multisig-participants') as HTMLInputElement).value); + + const request = new Promise(async resolve => { + // Generate multisig participants and combined address + await loadNimiq(); + + const myPublicKey = Nimiq.PublicKey.fromAny(publicKey); + + const publicKeys = new Array(numberOfKeys - 1).fill(0).map(() => Nimiq.PublicKey.derive(Nimiq.PrivateKey.generate())); + publicKeys.push(myPublicKey); + publicKeys.reverse(); // Move my public key to the front, so when slicing the signerPublicKeys, mine is included + + function calculateAddress(publicKeys, signersRequired) { + publicKeys.sort((a, b) => a.compare(b)); + const combinations = [...Nimiq.ArrayUtils.k_combinations(publicKeys, signersRequired)]; + const multiSigKeys = combinations.map(combination => Nimiq.PublicKey.sum(combination)); + multiSigKeys.sort((a, b) => a.compare(b)); + const merkleRoot = Nimiq.MerkleTree.computeRoot(multiSigKeys); + return Nimiq.Address.fromHash(merkleRoot); + } + + const result: SignMultisigTransactionRequest = { + appName: 'Hub Demos', + + signer: account.addresses[0].address, + + sender: calculateAddress(publicKeys, numberOfSigners).toUserFriendlyAddress(), + senderLabel: 'Our Multisig Wallet', + recipient: 'NQ82 HP54 C9D4 2FAG 69QD 6Q71 LURR 5187 0V3X', + recipientLabel: 'Best Friend', + value: parseInt((document.querySelector('#multisig-value') as HTMLInputElement).value) * 1e5, + fee: 0, + extraData: (document.querySelector('#multisig-data') as HTMLInputElement).value, + // flags: 0, + validityStartHeight: 0, + + multisigConfig: { + publicKeys: publicKeys.map(key => key.toHex()), + numberOfSigners, + signerPublicKeys: publicKeys.slice(0, numberOfSigners).map(key => key.toHex()), // Can be omitted when all publicKeys need to sign + secret: { + aggregatedSecret: '0000000000000000000000000000000000000000000000000000000000000000', + }, + aggregatedCommitment: '0000000000000000000000000000000000000000000000000000000000000000', + userName: (document.querySelector('#multisig-username') as HTMLInputElement).value, + }, + }; + resolve(result); + }); + + try { + const result = await demo.client.signMultisigTransaction(request, demo._defaultBehavior); + console.log('Result', result); + document.querySelector('#result')!.textContent = 'Transaction signed: ' + result!.signature; + } catch (e) { + console.error(e); + document.querySelector('#result')!.textContent = `Error: ${e.message || e}`; + } + }); + + document.querySelector('button#list-keyguard-keys')!.addEventListener('click', () => demo.listKeyguard()); + document.querySelector('button#setup-legacy-accounts')!.addEventListener('click', () => demo.setupLegacyAccounts()); document.querySelector('button#list-accounts')!.addEventListener('click', async () => demo.updateAccounts()); diff --git a/demos/index.html b/demos/index.html index c9ae06da..7c24f5e2 100644 --- a/demos/index.html +++ b/demos/index.html @@ -3,6 +3,11 @@ Nimiq Hub Demos + + + diff --git a/src/views/ConnectAccountSuccess.vue b/src/views/ConnectAccountSuccess.vue new file mode 100644 index 00000000..a4b7459b --- /dev/null +++ b/src/views/ConnectAccountSuccess.vue @@ -0,0 +1,70 @@ + + + From e17f3bef60565eebe6865ada2460f608586a93ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Fri, 21 Oct 2022 08:38:07 +0200 Subject: [PATCH 09/13] Add comment about permission additions --- src/lib/RpcApi.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/RpcApi.ts b/src/lib/RpcApi.ts index 4d404c34..ca1a92be 100644 --- a/src/lib/RpcApi.ts +++ b/src/lib/RpcApi.ts @@ -37,6 +37,9 @@ import { WalletInfo } from './WalletInfo'; export default class RpcApi { public static PERMISSIONED_REQUESTS: RequestType[] = [ RequestType.SIGN_MULTISIG_TRANSACTION, + // When adding new permissioned request types here, + // the ConnectAccount UI must be updated to be able + // to display these permissions to the user. ]; private static get HISTORY_KEY_RPC_STATE() { From eba1382d28f4022370e5843e2db531cf55febdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Fri, 21 Oct 2022 11:02:05 +0200 Subject: [PATCH 10/13] Narrow encryptionKey algorithm type --- client/PublicRequestTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/PublicRequestTypes.ts b/client/PublicRequestTypes.ts index 6604ddfa..5a137685 100644 --- a/client/PublicRequestTypes.ts +++ b/client/PublicRequestTypes.ts @@ -611,7 +611,7 @@ export interface ConnectedAccount { encryptionKey: { format: 'spki', keyData: Uint8Array, - algorithm: RsaHashedImportParams, + algorithm: { name: string, hash: string }, keyUsages: ['encrypt'], }; account: { From f7c05fb3702c3425bd1a5c26a1670266642ba61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 24 Oct 2022 12:40:42 +0200 Subject: [PATCH 11/13] Always show account selector Don't skip it even when only one account exists, as the user might want to log into another account. --- src/views/ChooseAddress.vue | 3 ++- src/views/ConnectAccount.vue | 17 +++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/views/ChooseAddress.vue b/src/views/ChooseAddress.vue index d5a21357..f11199dd 100644 --- a/src/views/ChooseAddress.vue +++ b/src/views/ChooseAddress.vue @@ -214,8 +214,9 @@ export default class ChooseAddress extends BitcoinSyncBaseView { staticStore.originalRouteName = RequestType.CHOOSE_ADDRESS; if (useReplace) { this.$router.replace({name: RequestType.ONBOARD}); + } else { + this.$router.push({name: RequestType.ONBOARD}); } - this.$router.push({name: RequestType.ONBOARD}); } private backgroundClass(address: string) { diff --git a/src/views/ConnectAccount.vue b/src/views/ConnectAccount.vue index 5edfc2a4..62169676 100644 --- a/src/views/ConnectAccount.vue +++ b/src/views/ConnectAccount.vue @@ -78,14 +78,11 @@ export default class ConnectAccount extends Vue { private AccountType = AccountType; - private showAccountSelector = false; + private showAccountSelector = true; private async created() { - if (this.wallets.length === 1 && this.wallets[0].type !== WalletType.LEDGER) { - this.setWallet(this.wallets[0], false); - } else { - // If more than one wallet exists or the one wallet is an unsupported LEDGER wallet, show the selector - this.showAccountSelector = true; + if (this.wallets.length === 0) { + this.goToOnboarding(); } } @@ -136,10 +133,14 @@ export default class ConnectAccount extends Vue { client.connectAccount(request); } - private goToOnboarding() { + private goToOnboarding(useReplace?: boolean) { // Redirect to onboarding staticStore.originalRouteName = RequestType.CONNECT_ACCOUNT; - this.$router.push({name: RequestType.ONBOARD}); + if (useReplace) { + this.$router.replace({name: RequestType.ONBOARD}); + } else { + this.$router.push({name: RequestType.ONBOARD}); + } } } From 4a740c6f65f84e534b6231c337694b161e39d6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 22 May 2023 12:01:58 +0200 Subject: [PATCH 12/13] Add encryption key params as required property to encrypted secrets --- client/PublicRequestTypes.ts | 8 ++++++++ package.json | 2 +- src/i18n/en.po | 8 ++++++++ src/lib/RequestParser.ts | 1 + src/lib/RequestTypes.ts | 5 +++++ yarn.lock | 21 +++------------------ 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/client/PublicRequestTypes.ts b/client/PublicRequestTypes.ts index 5a137685..819762ad 100644 --- a/client/PublicRequestTypes.ts +++ b/client/PublicRequestTypes.ts @@ -243,6 +243,12 @@ export interface SignedTransaction { }; } +export interface EncryptionKeyParams { + kdf: string; + iterations: number; + keySize: number; +} + export interface MultisigInfo { publicKeys: Bytes[]; numberOfSigners: number; @@ -252,6 +258,7 @@ export interface MultisigInfo { } | { encryptedSecrets: Bytes[]; bScalar: Bytes; + keyParams: EncryptionKeyParams; }; aggregatedCommitment: Bytes; userName?: string; @@ -613,6 +620,7 @@ export interface ConnectedAccount { keyData: Uint8Array, algorithm: { name: string, hash: string }, keyUsages: ['encrypt'], + keyParams: EncryptionKeyParams, }; account: { label: string; diff --git a/package.json b/package.json index bb74c2ec..6b6bd75e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@nimiq/electrum-client": "https://github.com/nimiq/electrum-client#build", "@nimiq/fastspot-api": "^1.8.0", "@nimiq/iqons": "^1.5.2", - "@nimiq/keyguard-client": "^1.6.0", + "@nimiq/keyguard-client": "https://gitpkg.now.sh/nimiq/keyguard?scripts.postinstall=cd%20client%20%26%26%20.%2Fbuild-gitpkg.sh&a62aa9f557e2ac4c0d7306828e423230f423a0f1", "@nimiq/ledger-api": "^2.3.0", "@nimiq/network-client": "^0.6.2", "@nimiq/oasis-api": "^1.1.1", diff --git a/src/i18n/en.po b/src/i18n/en.po index 69ea9611..cbe94ef3 100644 --- a/src/i18n/en.po +++ b/src/i18n/en.po @@ -96,6 +96,10 @@ msgstr "" msgid "All labels saved." msgstr "" +#: src/views/ConnectAccount.vue:8 +msgid "All private funds remain separate." +msgstr "" + #: src/components/CheckoutCardBitcoin.vue:30 #: src/components/CheckoutCardEthereum.vue:27 #: src/components/CheckoutCardNimiqExternal.vue:32 @@ -1270,6 +1274,10 @@ msgstr "" msgid "Your {app} app is outdated. Please update your Ledger firmware and {app} app using Ledger Live." msgstr "" +#: src/views/ConnectAccount.vue:7 +msgid "Your account will be used for approving only." +msgstr "" + #: src/views/LoginSuccess.vue:194 #: src/views/SignupLedger.vue:106 msgid "" diff --git a/src/lib/RequestParser.ts b/src/lib/RequestParser.ts index 3228b2dd..9ce0aa8d 100644 --- a/src/lib/RequestParser.ts +++ b/src/lib/RequestParser.ts @@ -766,6 +766,7 @@ export class RequestParser { (bytes) => parseBytes(bytes), ), bScalar: parseBytes(signMultisigTxRequest.multisigConfig.secret.bScalar), + keyParams: signMultisigTxRequest.multisigConfig.secret.keyParams, }, aggregatedCommitment: parseBytes(signMultisigTxRequest.multisigConfig.aggregatedCommitment), userName: signMultisigTxRequest.multisigConfig.userName, diff --git a/src/lib/RequestTypes.ts b/src/lib/RequestTypes.ts index aab94d9a..3b58659e 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -62,6 +62,11 @@ export interface ParsedMultisigInfo { } | { encryptedSecrets: Uint8Array[]; bScalar: Uint8Array; + keyParams: { + kdf: string; + iterations: number; + keySize: number; + }; }; aggregatedCommitment: Uint8Array; userName?: string; diff --git a/yarn.lock b/yarn.lock index 63dea06c..ca7573ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1978,11 +1978,6 @@ resolved "https://registry.yarnpkg.com/@nimiq/core-web/-/core-web-1.5.3.tgz#f0a4de59394f210f2c2d9cda8ee35c716847d40e" integrity sha512-W66SS9n3ygYgD52r1GJr1WtYYOkcZsqdtMmDCEwDvkrmeARnHs2sAvj77Wt4PQG8JA7GwK5svIJr6rGccCaekw== -"@nimiq/core-web@1.5.8": - version "1.5.8" - resolved "https://registry.yarnpkg.com/@nimiq/core-web/-/core-web-1.5.8.tgz#da8abef84c6d293cbb9ca495a77f08daa08886b9" - integrity sha512-MNpFbGZetz2eZcHtJVa5tpmLjPf65mTcJBRQkOHS8kWE+f+Z++hdnwWUBQCEAph4oC6bsE5JSuU7VpwcogN7+w== - "@nimiq/core-web@^1.6.0", "@nimiq/core-web@^1.6.1": version "1.6.1" resolved "https://registry.yarnpkg.com/@nimiq/core-web/-/core-web-1.6.1.tgz#97cb5b43b257c7f6f6808ef603e9bf686377241f" @@ -2033,14 +2028,9 @@ btoa "^1.1.2" node-lmdb "^0.9.6" -"@nimiq/keyguard-client@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@nimiq/keyguard-client/-/keyguard-client-1.6.0.tgz#e2cb22c1af5ae68dac74e5ead9e69aeaba2ffc8b" - integrity sha512-DDi+PycBtiTJO5Jwk3mHZzBP2FHARNCIR+9C6+uhw6kRlT/pcDS+vE6GeTGOwZsRGOtowLG3d9n43xWoPOOcGQ== - dependencies: - "@nimiq/core-web" "1.5.8" - "@nimiq/rpc" "^0.3.0" - "@opengsn/common" "^2.2.5" +"@nimiq/keyguard-client@https://gitpkg.now.sh/nimiq/keyguard?scripts.postinstall=cd%20client%20%26%26%20.%2Fbuild-gitpkg.sh&a62aa9f557e2ac4c0d7306828e423230f423a0f1": + version "1.0.0" + resolved "https://gitpkg.now.sh/nimiq/keyguard?scripts.postinstall=cd%20client%20%26%26%20.%2Fbuild-gitpkg.sh&a62aa9f557e2ac4c0d7306828e423230f423a0f1#b821554c4e4751e93cd9e287cd3617812e2a3ea2" "@nimiq/ledger-api@^2.3.0": version "2.3.0" @@ -2086,11 +2076,6 @@ resolved "https://registry.yarnpkg.com/@nimiq/rpc/-/rpc-0.1.5.tgz#53919b0a3a9abcdfebee0e865f4c663f72a8b8c2" integrity sha512-oTRThXzpbQOY8jz8h+2KXucWzW40nVfYBWROKXKBrSozhTG0nR+rzCbEm4ZyTC26b4RnmB6y4nYabUXi7gNWcA== -"@nimiq/rpc@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@nimiq/rpc/-/rpc-0.3.0.tgz#654c05acccc193b7d79fb09b2faf2114945ff872" - integrity sha512-je7fv+wP4nLEgTcZwu3FaGre22qkZ9AYGbStglVaJAxOH+3CvDnnOIa9IjGFaCEhtRQKRaQEvFqa5vN4IVnH+Q== - "@nimiq/rpc@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@nimiq/rpc/-/rpc-0.4.1.tgz#d5df1e426793afcdd8c407a2968442bbee874dbd" From f4e6c18decf990316d860c23f55d83d0453097ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Sat, 28 Sep 2024 15:13:46 +0200 Subject: [PATCH 13/13] Allow setting a custom senderType for SignMultisigTransaction --- client/PublicRequestTypes.ts | 1 + src/lib/RequestParser.ts | 2 ++ src/lib/RequestTypes.ts | 3 ++- src/views/SignMultisigTransaction.vue | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/PublicRequestTypes.ts b/client/PublicRequestTypes.ts index 819762ad..a16ce965 100644 --- a/client/PublicRequestTypes.ts +++ b/client/PublicRequestTypes.ts @@ -268,6 +268,7 @@ export interface SignMultisigTransactionRequest extends BasicRequest { signer: string; // Address sender: string; + senderType?: Nimiq.Account.Type; senderLabel: string; recipient: string; recipientType?: Nimiq.Account.Type; diff --git a/src/lib/RequestParser.ts b/src/lib/RequestParser.ts index 9ce0aa8d..f739c910 100644 --- a/src/lib/RequestParser.ts +++ b/src/lib/RequestParser.ts @@ -741,6 +741,7 @@ export class RequestParser { signer: Nimiq.Address.fromAny(signMultisigTxRequest.signer), sender: Nimiq.Address.fromString(signMultisigTxRequest.sender), + senderType: signMultisigTxRequest.senderType || Nimiq.Account.Type.BASIC, senderLabel: signMultisigTxRequest.senderLabel, recipient: Nimiq.Address.fromString(signMultisigTxRequest.recipient), recipientType: signMultisigTxRequest.recipientType || Nimiq.Account.Type.BASIC, @@ -996,6 +997,7 @@ export class RequestParser { signer: signMultisigTxRequest.signer.toUserFriendlyAddress(), sender: signMultisigTxRequest.sender.toUserFriendlyAddress(), + senderType: signMultisigTxRequest.senderType, senderLabel: signMultisigTxRequest.senderLabel, recipient: signMultisigTxRequest.recipient.toUserFriendlyAddress(), recipientType: signMultisigTxRequest.recipientType, diff --git a/src/lib/RequestTypes.ts b/src/lib/RequestTypes.ts index 3b58659e..0ab803d9 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -76,9 +76,10 @@ export interface ParsedSignMultisigTransactionRequest extends ParsedBasicRequest signer: Nimiq.Address; sender: Nimiq.Address; + senderType: Nimiq.Account.Type; senderLabel: string; recipient: Nimiq.Address; - recipientType?: Nimiq.Account.Type; + recipientType: Nimiq.Account.Type; recipientLabel?: string; value: number; fee?: number; diff --git a/src/views/SignMultisigTransaction.vue b/src/views/SignMultisigTransaction.vue index ea0a4565..94928b12 100644 --- a/src/views/SignMultisigTransaction.vue +++ b/src/views/SignMultisigTransaction.vue @@ -46,7 +46,7 @@ export default class SignMultisigTransaction extends Vue { sender: this.request.sender.serialize(), senderLabel: this.request.senderLabel, - senderType: Nimiq.Account.Type.BASIC, + senderType: this.request.senderType, recipient: this.request.recipient.serialize(), recipientType: this.request.recipientType, recipientLabel: this.request.recipientLabel,