From 43a6eaa5fac76e11558db21fcadcabb311addfd6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 4 Sep 2019 17:17:58 -0600 Subject: [PATCH 01/22] request type update for MultiCurrencyCheckout --- client/HubApi.ts | 6 +- client/RequestBehavior.ts | 3 +- demos/Demo.ts | 2 +- package.json | 4 +- src/lib/PublicRequestTypes.ts | 83 +++++++- src/lib/RequestParser.ts | 196 ++++++++++++++---- src/lib/RequestTypes.ts | 92 +++++--- src/lib/RpcApi.ts | 28 ++- src/lib/StaticStore.ts | 4 +- .../paymentOptions/BitcoinPaymentOptions.ts | 78 +++++++ src/lib/paymentOptions/EtherPaymentOptions.ts | 92 ++++++++ src/lib/paymentOptions/NimiqPaymentOptions.ts | 135 ++++++++++++ src/router.ts | 2 +- src/views/Checkout.vue | 4 +- src/views/ChooseAddress.vue | 3 +- src/views/ErrorHandler.vue | 2 +- src/views/ErrorHandlerUnsupportedLedger.vue | 3 +- src/views/ExportSuccess.vue | 4 +- src/views/Migrate.vue | 3 +- src/views/OnboardingSelector.vue | 3 +- src/views/SignMessage.vue | 4 +- src/views/SignTransactionLedger.vue | 55 ++++- tsconfig.json | 4 +- yarn.lock | 66 +++++- 24 files changed, 761 insertions(+), 115 deletions(-) create mode 100644 src/lib/paymentOptions/BitcoinPaymentOptions.ts create mode 100644 src/lib/paymentOptions/EtherPaymentOptions.ts create mode 100644 src/lib/paymentOptions/NimiqPaymentOptions.ts diff --git a/client/HubApi.ts b/client/HubApi.ts index ff7b247a0..e11a1c1b3 100644 --- a/client/HubApi.ts +++ b/client/HubApi.ts @@ -6,8 +6,8 @@ import { BehaviorType, } from './RequestBehavior'; import { RedirectRpcClient } from '@nimiq/rpc'; -import { RequestType } from '../src/lib/RequestTypes'; import { + RequestType, BasicRequest, SimpleRequest, OnboardRequest, @@ -23,11 +23,15 @@ import { SimpleResult, ExportResult, SignedMessage, + Currency, + PaymentMethod, } from '../src/lib/PublicRequestTypes'; export default class HubApi { // DB: Default Behavior public static readonly RequestType = RequestType; public static readonly RedirectRequestBehavior = RedirectRequestBehavior; + public static readonly Currency = Currency; + public static readonly PaymentMethod = PaymentMethod; public static readonly MSG_PREFIX = '\x16Nimiq Signed Message:\n'; private static get DEFAULT_ENDPOINT() { diff --git a/client/RequestBehavior.ts b/client/RequestBehavior.ts index f401461c0..f22a431c2 100644 --- a/client/RequestBehavior.ts +++ b/client/RequestBehavior.ts @@ -1,6 +1,5 @@ -import { RequestType } from '../src/lib/RequestTypes'; import { PostMessageRpcClient, RedirectRpcClient } from '@nimiq/rpc'; -import { ResultByRequestType } from '../src/lib/PublicRequestTypes'; +import { ResultByRequestType, RequestType } from '../src/lib/PublicRequestTypes'; export abstract class RequestBehavior { public static getAllowedOrigin(endpoint: string) { diff --git a/demos/Demo.ts b/demos/Demo.ts index 34f1c371d..9440cf39b 100644 --- a/demos/Demo.ts +++ b/demos/Demo.ts @@ -1,6 +1,5 @@ import { State, PostMessageRpcClient } from '@nimiq/rpc'; import HubApi from '../client/HubApi'; -import { RequestType } from '../src/lib/RequestTypes'; import { SimpleRequest, Account, @@ -11,6 +10,7 @@ import { SignMessageRequest, ExportRequest, RpcResult, + RequestType, } from '../src/lib/PublicRequestTypes'; import { PopupRequestBehavior, RedirectRequestBehavior } from '../client/RequestBehavior'; import { Utf8Tools } from '@nimiq/utils'; diff --git a/package.json b/package.json index e544bf77a..4f85240bd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "@nimiq/rpc": "^0.3.0", "@nimiq/style": "^0.7.2", "@nimiq/utils": "^0.3.2", - "@nimiq/vue-components": "https://github.com/nimiq/vue-components.git#build/accounts", + "@nimiq/vue-components": "https://github.com/nimiq/vue-components.git#sebastian/build/checkout/hub", + "big-integer": "^1.6.44", + "currency-codes": "^1.5.0", "vue": "^2.6.6", "vue-class-component": "^6.0.0", "vue-property-decorator": "^7.0.0", diff --git a/src/lib/PublicRequestTypes.ts b/src/lib/PublicRequestTypes.ts index ba602e20c..74f405afc 100644 --- a/src/lib/PublicRequestTypes.ts +++ b/src/lib/PublicRequestTypes.ts @@ -1,5 +1,24 @@ import { WalletType } from './WalletInfo'; -import { RequestType } from './RequestTypes'; +import { NimiqDirectPaymentOptions } from './paymentOptions/NimiqPaymentOptions'; +import { EtherDirectPaymentOptions } from './paymentOptions/EtherPaymentOptions'; +import { BitcoinDirectPaymentOptions } from './paymentOptions/BitcoinPaymentOptions'; + +export enum RequestType { + LIST = 'list', + MIGRATE = 'migrate', + CHECKOUT = 'checkout', + SIGN_MESSAGE = 'sign-message', + SIGN_TRANSACTION = 'sign-transaction', + ONBOARD = 'onboard', + SIGNUP = 'signup', + LOGIN = 'login', + EXPORT = 'export', + CHANGE_PASSWORD = 'change-password', + LOGOUT = 'logout', + ADD_ADDRESS = 'add-address', + RENAME = 'rename', + CHOOSE_ADDRESS = 'choose-address', +} export interface BasicRequest { appName: string; @@ -28,7 +47,8 @@ export interface SignTransactionRequest extends BasicRequest { validityStartHeight: number; // FIXME To be made optional when hub has its own network } -export interface CheckoutRequest extends BasicRequest { +export interface NimiqCheckoutRequest extends BasicRequest { + version?: 1 | undefined; shopLogoUrl?: string; sender?: string; forceSender?: boolean; @@ -41,6 +61,65 @@ export interface CheckoutRequest extends BasicRequest { validityDuration?: number; } +export enum PaymentMethod { + DIRECT, + OASIS, +} + +export enum Currency { + NIM = 'nim', + BTC = 'btc', + ETH = 'eth', +} + +export interface PaymentOptions { + type: T; + currency: C; + expires: number; + /** + * amount in the smallest unit of the currency specified in `currency`. + * i.e Luna for Currency.NIM and satoshi for Currency.BTC + */ + amount: string; +} + +export type AvailablePaymentOptions = NimiqDirectPaymentOptions + | EtherDirectPaymentOptions + | BitcoinDirectPaymentOptions; + +export interface MultiCurrencyCheckoutRequest extends BasicRequest { + version: 2; + /** + * must be located on the same origin as the one the request is sent from + */ + shopLogoUrl: string; + /** + * input is {currency, type} alongside the orde identifying parameters in the url. + * the called url must return a PaymentOptions object + */ + callbackUrl?: string; + extraData?: Uint8Array | string; + /** + * current time in milliseconds + */ + time: number; + /** + * ISO 4217 Code of the currency used on the calling site. + */ + fiatCurrency: string; + /** + * value in the currency specified by `fiatCurrency` + */ + fiatAmount: number; + /** + * array of available payment options. + * each currency can only be present once, and a Currency.NIM option must exist. + */ + paymentOptions: AvailablePaymentOptions[]; +} + +export type CheckoutRequest = NimiqCheckoutRequest | MultiCurrencyCheckoutRequest; + export interface SignedTransaction { serializedTx: string; // HEX hash: string; // HEX diff --git a/src/lib/RequestParser.ts b/src/lib/RequestParser.ts index 7f0695994..d5cb733fa 100644 --- a/src/lib/RequestParser.ts +++ b/src/lib/RequestParser.ts @@ -10,9 +10,11 @@ import { RenameRequest, ExportRequest, RpcRequest, + Currency, + PaymentMethod, + RequestType, } from './PublicRequestTypes'; import { - RequestType, ParsedBasicRequest, ParsedSimpleRequest, ParsedOnboardRequest, @@ -22,11 +24,21 @@ import { ParsedRenameRequest, ParsedExportRequest, ParsedRpcRequest, + ExtendedRpcRequest, + ExtendedCheckoutRequest, } from './RequestTypes'; +import { ParsedNimiqDirectPaymentOptions } from './paymentOptions/NimiqPaymentOptions'; +import { ParsedEtherDirectPaymentOptions } from './paymentOptions/EtherPaymentOptions'; +import { ParsedBitcoinDirectPaymentOptions } from './paymentOptions/BitcoinPaymentOptions'; +import CurrencyCode from 'currency-codes'; import { Utf8Tools } from '@nimiq/utils'; export class RequestParser { - public static parse(request: RpcRequest, state: State, requestType: RequestType): ParsedRpcRequest | null { + public static parse( + request: RpcRequest | ExtendedRpcRequest, + state: State, + requestType: RequestType, + ): ParsedRpcRequest | null { if (!request.appName) throw new Error('appName is required'); switch (requestType) { @@ -53,38 +65,130 @@ export class RequestParser { case RequestType.CHECKOUT: const checkoutRequest = request as CheckoutRequest; - if (!checkoutRequest.value) throw new Error('value is required'); - if (checkoutRequest.shopLogoUrl && new URL(checkoutRequest.shopLogoUrl).origin !== state.origin) { - throw new Error( - 'shopLogoUrl must have same origin as caller website. Image at ' + - checkoutRequest.shopLogoUrl + - ' is not on caller origin ' + - state.origin); + if (!checkoutRequest.version || checkoutRequest.version === 1) { + if (!checkoutRequest!.value) throw new Error('value is required'); + + if (checkoutRequest.shopLogoUrl && new URL(checkoutRequest.shopLogoUrl).origin !== state.origin) { + throw new Error( + 'shopLogoUrl must have same origin as caller website. Image at ' + + checkoutRequest.shopLogoUrl + + ' is not on caller origin ' + + state.origin); + } + + return { + kind: RequestType.CHECKOUT, + appName: checkoutRequest.appName, + shopLogoUrl: checkoutRequest.shopLogoUrl, + data: typeof checkoutRequest.extraData === 'string' + ? Utf8Tools.stringToUtf8ByteArray(checkoutRequest.extraData) + : checkoutRequest.extraData || new Uint8Array(0), + time: + new Date(), + paymentOptions: [new ParsedNimiqDirectPaymentOptions({ + currency: Currency.NIM, + type: PaymentMethod.DIRECT, + amount: checkoutRequest.value.toString(), + expires: 0, // unused for NimiqCheckoutRequests + protocolSpecific: { + recipient: checkoutRequest.recipient, + recipientType: checkoutRequest.recipientType || Nimiq.Account.Type.BASIC, + sender: checkoutRequest.sender, + forceSender: !!checkoutRequest.forceSender, + fee: checkoutRequest.fee || 0, + flags: checkoutRequest.flags, + validityDuration: !checkoutRequest.validityDuration ? TX_VALIDITY_WINDOW : Math.min( + TX_VALIDITY_WINDOW, + Math.max( + TX_MIN_VALIDITY_DURATION, + checkoutRequest.validityDuration, + ), + ), + }, + })], + } as ParsedCheckoutRequest; + } else { + if (checkoutRequest.version === 2) { + if (!checkoutRequest.paymentOptions.some((option) => option.currency === Currency.NIM)) { + throw new Error('CheckoutRequest must provide a NIM paymentOption.'); + } + + if (!checkoutRequest.shopLogoUrl + || new URL(checkoutRequest.shopLogoUrl).origin !== state.origin) { + throw new Error( + 'shopLogoUrl must have same origin as caller website. Image at ' + + checkoutRequest.shopLogoUrl + + ' is not on caller origin ' + + state.origin); + } + + if (!CurrencyCode.codes().includes(checkoutRequest.fiatCurrency)) { + throw new Error(`FiatCurrency ${checkoutRequest.fiatCurrency} not in ISO 4217`); + } + + if (!checkoutRequest.fiatAmount || checkoutRequest.fiatAmount <= 0) { + throw new Error('fiatAmount must be a positive non-zero number'); + } + + if (!checkoutRequest.callbackUrl) { + if (!checkoutRequest.paymentOptions.every( + (option) => !!option.protocolSpecific.recipient, + )) { + throw new Error('A callbackUrl or all recipients must be provided'); + } + } else { + if (new URL(checkoutRequest.callbackUrl).origin !== state.origin) { + throw new Error('callBackUrl must have the same origin as caller Website. ' + + checkoutRequest.callbackUrl + + ' is not on caller origin ' + + state.origin); + } + } + + const currencies: Set = new Set(); + + return { + kind: RequestType.CHECKOUT, + appName: checkoutRequest.appName, + shopLogoUrl: checkoutRequest.shopLogoUrl, + callbackUrl: checkoutRequest.callbackUrl, + data: typeof checkoutRequest.extraData === 'string' + ? Utf8Tools.stringToUtf8ByteArray(checkoutRequest.extraData) + : checkoutRequest.extraData || new Uint8Array(0), + time: checkoutRequest.time || + new Date(), + fiatCurrency: CurrencyCode.code(checkoutRequest.fiatCurrency), + fiatAmount: checkoutRequest.fiatAmount, + paymentOptions: checkoutRequest.paymentOptions.map((option) => { + if (!option.amount) { + throw new Error('Each paymentOption must provide an amount.'); + } + if (!option.expires) { + throw new Error('Each paymentOption must provide its expiration time'); + } + + if (currencies.has(option.currency)) { + throw new Error('Each Currency can only have one paymentOption'); + } else { + currencies.add(option.currency); + } + switch (option.type) { + case PaymentMethod.DIRECT: + switch (option.currency) { + case Currency.NIM: + return new ParsedNimiqDirectPaymentOptions(option); + case Currency.ETH: + return new ParsedEtherDirectPaymentOptions(option); + case Currency.BTC: + return new ParsedBitcoinDirectPaymentOptions(option); + default: + throw new Error(`Currency ${(option as any).currency} not supported`); + } + default: + throw new Error(`PaymentMethod not supported`); + } + }), + } as ParsedCheckoutRequest; + } } - return { - kind: RequestType.CHECKOUT, - appName: checkoutRequest.appName, - shopLogoUrl: checkoutRequest.shopLogoUrl, - sender: checkoutRequest.sender - ? Nimiq.Address.fromUserFriendlyAddress(checkoutRequest.sender) - : undefined, - forceSender: !!checkoutRequest.forceSender, - recipient: Nimiq.Address.fromUserFriendlyAddress(checkoutRequest.recipient), - recipientType: checkoutRequest.recipientType || Nimiq.Account.Type.BASIC, - value: checkoutRequest.value, - fee: checkoutRequest.fee || 0, - data: typeof checkoutRequest.extraData === 'string' - ? Utf8Tools.stringToUtf8ByteArray(checkoutRequest.extraData) - : checkoutRequest.extraData || new Uint8Array(0), - flags: checkoutRequest.flags || Nimiq.Transaction.Flag.NONE, - validityDuration: !checkoutRequest.validityDuration ? TX_VALIDITY_WINDOW : Math.min( - TX_VALIDITY_WINDOW, - Math.max( - TX_MIN_VALIDITY_DURATION, - checkoutRequest.validityDuration, - ), - ), - } as ParsedCheckoutRequest; case RequestType.ONBOARD: const onboardRequest = request as OnboardRequest; return { @@ -154,7 +258,8 @@ export class RequestParser { } } - public static raw(request: ParsedRpcRequest): RpcRequest | null { + public static raw(request: ParsedRpcRequest) + : RpcRequest | ExtendedRpcRequest | null { switch (request.kind) { case RequestType.SIGN_TRANSACTION: const signTransactionRequest = request as ParsedSignTransactionRequest; @@ -173,17 +278,22 @@ export class RequestParser { const checkoutRequest = request as ParsedCheckoutRequest; return { appName: checkoutRequest.appName, + version: 2, shopLogoUrl: checkoutRequest.shopLogoUrl, - sender: checkoutRequest.sender ? checkoutRequest.sender.toUserFriendlyAddress() : undefined, - forceSender: checkoutRequest.forceSender, - recipient: checkoutRequest.recipient.toUserFriendlyAddress(), - recipientType: checkoutRequest.recipientType, - value: checkoutRequest.value, - fee: checkoutRequest.fee, extraData: checkoutRequest.data, - flags: checkoutRequest.flags, - validityDuration: checkoutRequest.validityDuration, - } as CheckoutRequest; + callbackUrl: checkoutRequest.callbackUrl, + time: checkoutRequest.time, + fiatAmount: checkoutRequest.fiatAmount ? checkoutRequest.fiatAmount : undefined, + fiatCurrency: checkoutRequest.fiatCurrency ? checkoutRequest.fiatCurrency.code : undefined, + paymentOptions: checkoutRequest.paymentOptions.map((option) => { + switch (option.type) { + case PaymentMethod.DIRECT: + return option.raw(); + default: + throw new Error('paymentOption.type not supported'); + } + }), + } as ExtendedCheckoutRequest; case RequestType.ONBOARD: const onboardRequest = request as ParsedOnboardRequest; return { diff --git a/src/lib/RequestTypes.ts b/src/lib/RequestTypes.ts index f929b6b03..4a0b03db7 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -1,19 +1,27 @@ -export enum RequestType { - LIST = 'list', - MIGRATE = 'migrate', - CHECKOUT = 'checkout', - SIGN_MESSAGE = 'sign-message', - SIGN_TRANSACTION = 'sign-transaction', - ONBOARD = 'onboard', - SIGNUP = 'signup', - LOGIN = 'login', - EXPORT = 'export', - CHANGE_PASSWORD = 'change-password', - LOGOUT = 'logout', - ADD_ADDRESS = 'add-address', - RENAME = 'rename', - CHOOSE_ADDRESS = 'choose-address', -} +import CurrencyCode from 'currency-codes'; +import { + RequestType, + PaymentOptions, + Currency, + PaymentMethod, + MultiCurrencyCheckoutRequest, + AvailablePaymentOptions, +} from './PublicRequestTypes'; +import { + ParsedNimiqDirectPaymentOptions, + ExtendedNimiqDirectPaymentOptions, + NimiqDirectPaymentOptions, + } from './paymentOptions/NimiqPaymentOptions'; +import { + ParsedEtherDirectPaymentOptions, + EtherDirectPaymentOptions, +} from './paymentOptions/EtherPaymentOptions'; +import { + ParsedBitcoinDirectPaymentOptions, + BitcoinDirectPaymentOptions, +} from './paymentOptions/BitcoinPaymentOptions'; + +export type Omit = Pick>; export interface ParsedBasicRequest { kind: RequestType; @@ -39,19 +47,55 @@ export interface ParsedSignTransactionRequest extends ParsedBasicRequest { validityStartHeight: number; // FIXME To be made optional when hub has its own network } +export interface ParsedPaymentOptions { + currency: C; + type: T; + expires: number; + paymentLink: string; + raw(): PaymentOptions; +} + +export abstract class ParsedPaymentOptions + implements ParsedPaymentOptions { + public readonly abstract digits: number; + public readonly abstract minDigits: number; + public readonly abstract maxDigits: number; + public expires: number; + + public constructor(option: PaymentOptions) { + this.expires = option.expires; + } + + public abstract update(option: PaymentOptions): void; +} + +export type AvailableParsedPaymentOptions = ParsedNimiqDirectPaymentOptions + | ParsedEtherDirectPaymentOptions + | ParsedBitcoinDirectPaymentOptions; + export interface ParsedCheckoutRequest extends ParsedBasicRequest { shopLogoUrl?: string; - sender?: Nimiq.Address; - forceSender: boolean; - recipient: Nimiq.Address; - recipientType: Nimiq.Account.Type; - value: number; - fee: number; + callbackUrl?: string; data: Uint8Array; - flags: number; - validityDuration: number; + time: number; + fiatCurrency?: CurrencyCode.CurrencyCodeRecord; + fiatAmount?: number; + paymentOptions: AvailableParsedPaymentOptions[]; } +export type ExtendedPaymentOptions = ExtendedNimiqDirectPaymentOptions + | EtherDirectPaymentOptions + | BitcoinDirectPaymentOptions; + +export type ExtendedCheckoutRequest = Omit & { + fiatCurrency?: CurrencyCode.CurrencyCodeRecord; + fiatAmount?: number; + paymentOptions: ExtendedPaymentOptions[]; +}; + +export type ExtendedRpcRequest = ExtendedCheckoutRequest; + export interface ParsedSignMessageRequest extends ParsedBasicRequest { signer?: Nimiq.Address; message: string | Uint8Array; diff --git a/src/lib/RpcApi.ts b/src/lib/RpcApi.ts index 19ca216f2..a7f0007d2 100644 --- a/src/lib/RpcApi.ts +++ b/src/lib/RpcApi.ts @@ -8,10 +8,10 @@ import { ParsedSignMessageRequest, ParsedSignTransactionRequest, ParsedSimpleRequest, - RequestType, } from './RequestTypes'; import { RequestParser } from './RequestParser'; -import { RpcRequest, RpcResult } from './PublicRequestTypes'; +import { RpcRequest, RpcResult, Currency, RequestType } from './PublicRequestTypes'; +import { ParsedNimiqDirectPaymentOptions } from './paymentOptions/NimiqPaymentOptions'; import { KeyguardClient, KeyguardCommand, @@ -225,12 +225,24 @@ export default class RpcApi { } } else if (requestType === RequestType.CHECKOUT) { const checkoutRequest = request as ParsedCheckoutRequest; - accountRequired = checkoutRequest.forceSender; - if (checkoutRequest.sender) { - account = this._store.getters.findWalletByAddress( - checkoutRequest.sender.toUserFriendlyAddress(), - true, - ); + // forceSender only applies to non-multi-currency checkouts. + if (checkoutRequest.paymentOptions.length === 1 + && checkoutRequest.paymentOptions[0].currency === Currency.NIM) { + + /** + * Later on can potentially be ParsedNimiqOasisPaymentOptions. + * If it will contain the forceSender flag as well it should not be an issue. + */ + const protocolSpecific = + (checkoutRequest.paymentOptions[0] as ParsedNimiqDirectPaymentOptions).protocolSpecific; + + accountRequired = protocolSpecific.forceSender; + if (protocolSpecific.sender) { + account = this._store.getters.findWalletByAddress( + protocolSpecific.sender.toUserFriendlyAddress(), + true, + ); + } } } if (accountRequired && !account) { diff --git a/src/lib/StaticStore.ts b/src/lib/StaticStore.ts index adae6eac7..a7422c676 100644 --- a/src/lib/StaticStore.ts +++ b/src/lib/StaticStore.ts @@ -1,7 +1,7 @@ import Vue from 'vue'; import { createDecorator } from 'vue-class-component'; -import { ParsedRpcRequest, RequestType } from './RequestTypes'; -import { RpcResult } from './PublicRequestTypes'; +import { ParsedRpcRequest } from './RequestTypes'; +import { RpcResult, RequestType } from './PublicRequestTypes'; import { State as RpcState } from '@nimiq/rpc'; import { Request as KeyguardRequest } from '@nimiq/keyguard-client'; diff --git a/src/lib/paymentOptions/BitcoinPaymentOptions.ts b/src/lib/paymentOptions/BitcoinPaymentOptions.ts new file mode 100644 index 000000000..cc79e868a --- /dev/null +++ b/src/lib/paymentOptions/BitcoinPaymentOptions.ts @@ -0,0 +1,78 @@ +import { CurrencyCodeRecord } from 'currency-codes'; +import { Currency, PaymentMethod, PaymentOptions } from '../PublicRequestTypes'; +import { ParsedPaymentOptions } from '../RequestTypes'; + +export interface BitcoinDirectPaymentOptions extends PaymentOptions { + protocolSpecific: { + fee?: number; + recipient?: string; + }; +} + +export class ParsedBitcoinDirectPaymentOptions extends ParsedPaymentOptions { + public readonly digits: number = 8; + public readonly minDigits: number = 3; + public readonly maxDigits: number = 5; + public readonly currency: Currency.BTC = Currency.BTC; + public readonly type: PaymentMethod.DIRECT = PaymentMethod.DIRECT; + public amount: number; + public protocolSpecific: { + fee?: number; + recipient?: string; + }; + + public get total(): number { + return (this.amount + this.fee); + } + + public get fee(): number { + return this.protocolSpecific.fee || 0; + } + + public get paymentLink() { + return ''; // TODO + } + + public constructor(option: BitcoinDirectPaymentOptions) { + super(option); + this.amount = Number.parseInt(option.amount, 10); + this.protocolSpecific = { + fee: option.protocolSpecific.fee, + recipient: option.protocolSpecific.recipient, + }; + } + + public update(options: BitcoinDirectPaymentOptions) { + const newOptions = new ParsedBitcoinDirectPaymentOptions(options); + this.expires = newOptions.expires || this.expires; + this.amount = newOptions.amount || this.amount; + this.protocolSpecific = { + fee: newOptions.fee || this.protocolSpecific.fee, + recipient: newOptions.protocolSpecific.recipient || this.protocolSpecific.recipient, + }; + } + + public fiatFee(fiatAmount: number, fiatCurrency: CurrencyCodeRecord): number { + if (!this.amount || !fiatAmount || !fiatCurrency) { + throw new Error('amount, fiatAmount and fiatCurrency must be provided'); + return 0; + } + if (!this.fee) { + return 0; + } + return this.fee * fiatAmount / this.amount; + } + + public raw(): BitcoinDirectPaymentOptions { + return { + currency: this.currency, + type: this.type, + expires: this.expires, + amount: this.amount.toString(), + protocolSpecific: { + fee: this.protocolSpecific.fee, + recipient: this.protocolSpecific.recipient, + }, + }; + } +} diff --git a/src/lib/paymentOptions/EtherPaymentOptions.ts b/src/lib/paymentOptions/EtherPaymentOptions.ts new file mode 100644 index 000000000..f40703e1c --- /dev/null +++ b/src/lib/paymentOptions/EtherPaymentOptions.ts @@ -0,0 +1,92 @@ +import { CurrencyCodeRecord } from 'currency-codes'; +import bigInt from 'big-integer'; +import { Currency, PaymentMethod, PaymentOptions } from '../PublicRequestTypes'; +import { ParsedPaymentOptions } from '../RequestTypes'; + +export interface EtherDirectPaymentOptions extends PaymentOptions { + protocolSpecific: { + gasLimit?: number; + gasPrice?: string; + recipient?: string; + }; +} + +export class ParsedEtherDirectPaymentOptions extends ParsedPaymentOptions { + public readonly digits: number = 18; + public readonly minDigits: number = 1; + public readonly maxDigits: number = 3; + public readonly currency: Currency.ETH = Currency.ETH; + public readonly type: PaymentMethod.DIRECT = PaymentMethod.DIRECT; + public amount: bigInt.BigInteger; + public protocolSpecific: { + gasLimit?: number; + gasPrice?: bigInt.BigInteger; + recipient?: string; + }; + + public get total(): bigInt.BigInteger { + return this.amount.add(this.fee); + } + + public get fee(): bigInt.BigInteger { + return this.protocolSpecific.gasPrice!.times(this.protocolSpecific.gasLimit!) || bigInt(0); + } + + public get paymentLink() { + return ''; // TODO + } + + public constructor(option: EtherDirectPaymentOptions) { + super(option); + this.amount = bigInt(option.amount); + this.protocolSpecific = { + gasLimit: option.protocolSpecific.gasLimit, + gasPrice: option.protocolSpecific.gasPrice + ? bigInt(option.protocolSpecific.gasPrice) + : undefined, + recipient: option.protocolSpecific.recipient, + }; + } + + public update(options: EtherDirectPaymentOptions) { + const newOptions = new ParsedEtherDirectPaymentOptions(options); + this.expires = newOptions.expires || this.expires; + this.amount = newOptions.amount || this.amount; + this.protocolSpecific = { + gasLimit: newOptions.protocolSpecific.gasLimit || this.protocolSpecific.gasLimit, + gasPrice: newOptions.protocolSpecific.gasPrice || this.protocolSpecific.gasPrice, + recipient: newOptions.protocolSpecific.recipient || this.protocolSpecific.recipient, + }; + } + + public fiatFee(fiatAmount: number, fiatCurrency: CurrencyCodeRecord): number { + if (!this.amount || !fiatAmount || !fiatCurrency) { + throw new Error('amount, fiatAmount and fiatCurrency must be provided'); + } + + if (this.fee.isZero()) { + return 0; + } + + return this.fee + .times(bigInt(fiatAmount * (10 ** fiatCurrency.digits))) + .divide(this.amount) // integer division loss of precision here. + .valueOf() / (10 ** fiatCurrency.digits); + } + + public raw(): EtherDirectPaymentOptions { + return { + currency: this.currency, + type: this.type, + expires: this.expires, + amount: this.amount.toString(), + protocolSpecific: { + gasLimit: this.protocolSpecific.gasLimit, + gasPrice: this.protocolSpecific.gasPrice + ? this.protocolSpecific.gasPrice.toString() + : undefined, + recipient: this.protocolSpecific.recipient, + }, + }; + } +} diff --git a/src/lib/paymentOptions/NimiqPaymentOptions.ts b/src/lib/paymentOptions/NimiqPaymentOptions.ts new file mode 100644 index 000000000..354139a18 --- /dev/null +++ b/src/lib/paymentOptions/NimiqPaymentOptions.ts @@ -0,0 +1,135 @@ +import { CurrencyCodeRecord } from 'currency-codes'; +import { TX_VALIDITY_WINDOW, TX_MIN_VALIDITY_DURATION } from '../Constants'; +import { Currency, PaymentMethod, PaymentOptions } from '../PublicRequestTypes'; +import { Omit, ParsedPaymentOptions } from '../RequestTypes'; + +export interface NimiqDirectPaymentOptions extends PaymentOptions { + protocolSpecific: { + fee?: number; + validityDuration?: number; + sender?: string; + forceSender?: boolean; + recipient?: string; + recipientType?: Nimiq.Account.Type; + }; +} + +export type ExtendedNimiqDirectPaymentOptions = Omit & { + protocolSpecific: { + fee?: number; + validityDuration?: number; + flags: number; + recipient?: string; + recipientType?: Nimiq.Account.Type; + sender?: string; + forceSender: boolean; + }; +}; + +export class ParsedNimiqDirectPaymentOptions extends ParsedPaymentOptions { + public readonly digits: number = 5; + public readonly minDigits: number = 0; + public readonly maxDigits: number = 0; + public readonly currency: Currency.NIM = Currency.NIM; + public readonly type: PaymentMethod.DIRECT = PaymentMethod.DIRECT; + + public amount: number; + public protocolSpecific: { + sender?: Nimiq.Address, + forceSender?: boolean, + fee?: number, + flags: number, + recipient?: Nimiq.Address, + recipientType?: Nimiq.Account.Type, + validityDuration?: number, + }; + + public get paymentLink() { + return ''; // TODO + } + + public constructor(option: NimiqDirectPaymentOptions | ExtendedNimiqDirectPaymentOptions) { + super(option); + this.amount = parseInt(option.amount, 10); + this.protocolSpecific = { + sender: option.protocolSpecific.sender + ? Nimiq.Address.fromUserFriendlyAddress( + option.protocolSpecific.sender) + : undefined, + forceSender: !!option.protocolSpecific.forceSender, + fee: option.protocolSpecific.fee, + flags: (option as ExtendedNimiqDirectPaymentOptions) + .protocolSpecific.flags || Nimiq.Transaction.Flag.NONE, + recipient: option.protocolSpecific.recipient + ? Nimiq.Address.fromUserFriendlyAddress( + option.protocolSpecific.recipient) + : undefined, + recipientType: option.protocolSpecific.recipientType, + validityDuration: !option.protocolSpecific.validityDuration + ? TX_VALIDITY_WINDOW + : Math.min( + TX_VALIDITY_WINDOW, + Math.max( + TX_MIN_VALIDITY_DURATION, + option.protocolSpecific.validityDuration, + ), + ), + }; + } + + public update(options: NimiqDirectPaymentOptions) { + const newOptions = new ParsedNimiqDirectPaymentOptions(options); + this.expires = newOptions.expires || this.expires; + this.amount = newOptions.amount || this.amount; + this.protocolSpecific = { + sender: newOptions.protocolSpecific.sender || this.protocolSpecific.sender, + forceSender: newOptions.protocolSpecific.forceSender || this.protocolSpecific.forceSender, + fee: newOptions.protocolSpecific.fee || this.protocolSpecific.fee, + flags: newOptions.protocolSpecific.flags || this.protocolSpecific.flags, + recipient: newOptions.protocolSpecific.recipient || this.protocolSpecific.recipient, + recipientType: newOptions.protocolSpecific.recipientType || this.protocolSpecific.recipientType, + validityDuration: newOptions.protocolSpecific.validityDuration || this.protocolSpecific.validityDuration, + }; + } + + public get total(): number { + return this.amount + this.fee; + } + + public get fee(): number { + return this.protocolSpecific.fee || 0; + } + + public fiatFee(fiatAmount: number, fiatCurrency: CurrencyCodeRecord): number { + if (!this.amount || !fiatAmount || !fiatCurrency) { + throw new Error('amount, fiatAmount and fiatCurrency must be provided'); + return 0; + } + if (!this.fee) { + return 0; + } + return this.fee * fiatAmount / this.amount; + } + + public raw(): ExtendedNimiqDirectPaymentOptions { + return { + currency: this.currency, + type: this.type, + expires: this.expires, + amount: this.amount.toString(), + protocolSpecific: { + recipient: this.protocolSpecific.recipient + ? this.protocolSpecific.recipient.toUserFriendlyAddress() + : undefined, + fee: this.protocolSpecific.fee, + validityDuration: this.protocolSpecific.validityDuration, + sender: this.protocolSpecific.sender + ? this.protocolSpecific.sender.toUserFriendlyAddress() + : undefined, + flags: this.protocolSpecific.flags, + recipientType: this.protocolSpecific.recipientType, + forceSender: !!this.protocolSpecific.forceSender, + }, + }; + } +} diff --git a/src/router.ts b/src/router.ts index af85ba6c2..2f2ee6dd9 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,6 +1,6 @@ import Vue from 'vue'; import Router from 'vue-router'; -import { RequestType } from '@/lib/RequestTypes'; +import { RequestType } from '@/lib/PublicRequestTypes'; import { KeyguardCommand } from '@nimiq/keyguard-client'; const SignTransaction = () => import(/*webpackChunkName: "sign-transaction"*/ './views/SignTransaction.vue'); diff --git a/src/views/Checkout.vue b/src/views/Checkout.vue index b5a7e23a5..3656aeb6f 100644 --- a/src/views/Checkout.vue +++ b/src/views/Checkout.vue @@ -53,8 +53,8 @@ import { Component, Vue } from 'vue-property-decorator'; import { PaymentInfoLine, AccountSelector, AccountDetails, SmallPage } from '@nimiq/vue-components'; import { TransferIcon, ArrowLeftSmallIcon } from '@nimiq/vue-components'; -import { ParsedCheckoutRequest, RequestType } from '../lib/RequestTypes'; -import { Account } from '../lib/PublicRequestTypes'; +import { ParsedCheckoutRequest } from '../lib/RequestTypes'; +import { Account, RequestType } from '../lib/PublicRequestTypes'; import { State as RpcState } from '@nimiq/rpc'; import staticStore, { Static } from '../lib/StaticStore'; import { WalletStore } from '../lib/WalletStore'; diff --git a/src/views/ChooseAddress.vue b/src/views/ChooseAddress.vue index 465456176..b140dbfc1 100644 --- a/src/views/ChooseAddress.vue +++ b/src/views/ChooseAddress.vue @@ -25,8 +25,7 @@ import { Component, Vue } from 'vue-property-decorator'; import { Getter, Mutation } from 'vuex-class'; import { SmallPage, AccountSelector, ArrowLeftSmallIcon } from '@nimiq/vue-components'; -import { RequestType } from '../lib/RequestTypes'; -import { SimpleRequest, Account, Address } from '../lib/PublicRequestTypes'; +import { SimpleRequest, Account, Address, RequestType } from '../lib/PublicRequestTypes'; import staticStore, { Static } from '@/lib/StaticStore'; import { WalletStore } from '@/lib/WalletStore'; import { WalletInfo } from '../lib/WalletInfo'; diff --git a/src/views/ErrorHandler.vue b/src/views/ErrorHandler.vue index 893d15ece..610679acf 100644 --- a/src/views/ErrorHandler.vue +++ b/src/views/ErrorHandler.vue @@ -5,12 +5,12 @@ import { Component, Vue } from 'vue-property-decorator'; import { State, Getter } from 'vuex-class'; import staticStore, { Static } from '../lib/StaticStore'; import { - RequestType, ParsedRpcRequest, ParsedSimpleRequest, ParsedSignMessageRequest, ParsedSignTransactionRequest, } from '../lib/RequestTypes'; +import { RequestType } from '../lib/PublicRequestTypes'; import { Errors } from '@nimiq/keyguard-client'; import { WalletStore } from '../lib/WalletStore'; import KeyguardClient from '@nimiq/keyguard-client'; diff --git a/src/views/ErrorHandlerUnsupportedLedger.vue b/src/views/ErrorHandlerUnsupportedLedger.vue index fae500303..a3e499929 100644 --- a/src/views/ErrorHandlerUnsupportedLedger.vue +++ b/src/views/ErrorHandlerUnsupportedLedger.vue @@ -18,7 +18,8 @@ import { Component, Vue } from 'vue-property-decorator'; import { ArrowLeftSmallIcon, SmallPage } from '@nimiq/vue-components'; import StatusScreen from '../components/StatusScreen.vue'; import { Static } from '../lib/StaticStore'; -import { ParsedBasicRequest, RequestType } from '../lib/RequestTypes'; +import { ParsedBasicRequest } from '../lib/RequestTypes'; +import { RequestType } from '../lib/PublicRequestTypes'; import { ERROR_CANCELED } from '../lib/Constants'; @Component({components: {SmallPage, StatusScreen, ArrowLeftSmallIcon}}) diff --git a/src/views/ExportSuccess.vue b/src/views/ExportSuccess.vue index a598a251c..63b502fad 100644 --- a/src/views/ExportSuccess.vue +++ b/src/views/ExportSuccess.vue @@ -14,8 +14,8 @@