diff --git a/src/app/components/from-to/from-to.component.html b/src/app/components/from-to/from-to.component.html index 2a89f56..c75c31f 100644 --- a/src/app/components/from-to/from-to.component.html +++ b/src/app/components/from-to/from-to.component.html @@ -52,7 +52,7 @@ - + @@ -61,11 +61,11 @@ - +
Operation #{{ i + 1 }}
-
+ @@ -94,7 +94,7 @@
- + diff --git a/src/app/components/from-to/from-to.component.ts b/src/app/components/from-to/from-to.component.ts index 1818052..e63c113 100644 --- a/src/app/components/from-to/from-to.component.ts +++ b/src/app/components/from-to/from-to.component.ts @@ -1,6 +1,10 @@ import { Component, Input, Output, EventEmitter } from '@angular/core' -import { getProtocolByIdentifier, IAirGapTransaction, MainProtocolSymbols } from '@airgap/coinlib-core' -import { FullOperationGroup } from 'src/extension/tezos-types' +import { + getProtocolByIdentifier, + IAirGapTransaction, + MainProtocolSymbols, + TezosWrappedOperation +} from '@airgap/coinlib-core' import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms' import { FeeConverterPipe } from 'src/app/pipes/fee-converter/fee-converter.pipe' import { isInjectableOperation } from 'src/app/types/tezos-operation' @@ -17,10 +21,12 @@ export class FromToComponent { public transactions: IAirGapTransaction[] | undefined @Input() - public operationGroup: FullOperationGroup | undefined + public tezosWrappedOperation: TezosWrappedOperation | undefined @Output() - public readonly onOperationGroupUpdate: EventEmitter = new EventEmitter() + public readonly onWrappedOperationUpdate: EventEmitter = new EventEmitter< + TezosWrappedOperation + >() constructor(private readonly formBuilder: FormBuilder, private readonly feeConverter: FeeConverterPipe) {} @@ -34,17 +40,21 @@ export class FromToComponent { } public async initForms() { - if (this.operationGroup === undefined) { + if (this.tezosWrappedOperation === undefined) { return } + const protocol = getProtocolByIdentifier(MainProtocolSymbols.XTZ) this.formGroup = this.formBuilder.group({ operations: this.formBuilder.array( - this.operationGroup.contents.map(operation => { + this.tezosWrappedOperation.contents.map(operation => { if (!isInjectableOperation(operation)) { return this.formBuilder.group({}) } - const feeValue = this.feeConverter.transform(operation.fee, { protocolIdentifier: MainProtocolSymbols.XTZ, appendSymbol: false }) + const feeValue = this.feeConverter.transform(operation.fee, { + protocolIdentifier: MainProtocolSymbols.XTZ, + appendSymbol: false + }) const feeControl = this.formBuilder.control(feeValue, [ Validators.required, Validators.pattern(`^[0-9]+(\.[0-9]{1,${protocol.feeDecimals}})*$`) @@ -68,23 +78,27 @@ export class FromToComponent { } public updateOperationGroup() { - if (this.operationGroup === undefined) { + if (this.tezosWrappedOperation === undefined) { return } - this.operationGroup.contents = this.operationGroup.contents.map((operation, index) => { + this.tezosWrappedOperation.contents = this.tezosWrappedOperation.contents.map((operation, index) => { if (!isInjectableOperation(operation) || this.formGroup === undefined) { return operation } const group = (this.formGroup.controls.operations as FormArray).controls[index] as FormGroup - const fee = this.feeConverter.transform(group.controls.fee.value, { protocolIdentifier: MainProtocolSymbols.XTZ, reverse: true, appendSymbol: false }) + const fee = this.feeConverter.transform(group.controls.fee.value, { + protocolIdentifier: MainProtocolSymbols.XTZ, + reverse: true, + appendSymbol: false + }) return { ...operation, fee, gas_limit: String(group.controls.gasLimit.value), - storage_limit: String(group.controls.storageLimit.value), + storage_limit: String(group.controls.storageLimit.value) } }) - this.onOperationGroupUpdate.emit(this.operationGroup) + this.onWrappedOperationUpdate.emit(this.tezosWrappedOperation) this.advanced = false } } diff --git a/src/app/pages/add-ledger-connection/add-ledger-connection.page.ts b/src/app/pages/add-ledger-connection/add-ledger-connection.page.ts index d891c1f..416d898 100644 --- a/src/app/pages/add-ledger-connection/add-ledger-connection.page.ts +++ b/src/app/pages/add-ledger-connection/add-ledger-connection.page.ts @@ -65,6 +65,12 @@ export class AddLedgerConnectionPage implements OnInit { this.confirmText = 'Confirm Transaction on your ledger.' } + if (this.targetMethod === Action.DRY_RUN) { + this.title = 'Simulate Operation' + this.showDerivationPath = false + this.confirmText = 'Confirm Simulation on your ledger.' + } + return this.connect() } @@ -102,6 +108,13 @@ export class AddLedgerConnectionPage implements OnInit { } await this.walletService.addAndActiveWallet(walletInfo) } + } else if (this.targetMethod === Action.DRY_RUN) { + const { data }: ExtensionMessageOutputPayload = response as ExtensionMessageOutputPayload< + Action.DRY_RUN + > + setTimeout(() => { + return this.modalController.dismiss(data) + }, 2000) } setTimeout(() => { return this.dismiss(true) diff --git a/src/app/pages/beacon-request/beacon-request.page.html b/src/app/pages/beacon-request/beacon-request.page.html index 3abd401..e166dc5 100644 --- a/src/app/pages/beacon-request/beacon-request.page.html +++ b/src/app/pages/beacon-request/beacon-request.page.html @@ -84,9 +84,7 @@

- +
@@ -104,8 +102,8 @@

diff --git a/src/app/pages/beacon-request/beacon-request.page.ts b/src/app/pages/beacon-request/beacon-request.page.ts index fe87256..a3aaf77 100644 --- a/src/app/pages/beacon-request/beacon-request.page.ts +++ b/src/app/pages/beacon-request/beacon-request.page.ts @@ -13,7 +13,7 @@ import { import { ModalOptions } from '@ionic/core' import { Component, OnInit } from '@angular/core' import { AlertController, ModalController, ToastController } from '@ionic/angular' -import { IAirGapTransaction, TezosProtocol } from '@airgap/coinlib-core' +import { IAirGapTransaction, TezosProtocol, TezosWrappedOperation } from '@airgap/coinlib-core' import { take } from 'rxjs/operators' import { ChromeMessagingService } from 'src/app/services/chrome-messaging.service' import { WalletService } from 'src/app/services/local-wallet.service' @@ -24,7 +24,6 @@ import { AddLedgerConnectionPage } from '../add-ledger-connection/add-ledger-con import { ErrorPage } from '../error/error.page' import { AirGapOperationProvider } from 'src/extension/AirGapSigner' import { DryRunPreviewPage } from '../dry-run-preview/dry-run-preview.page' -import { FullOperationGroup } from 'src/extension/tezos-types' @Component({ selector: 'beacon-request', @@ -41,7 +40,7 @@ export class BeaconRequestPage implements OnInit { public address: string = '' public inputs?: any public transactionsPromise: Promise | undefined - public operationGroupPromise: Promise | undefined + public wrappedOperationPromise: Promise | undefined public responseHandler: (() => Promise) | undefined @@ -148,11 +147,11 @@ export class BeaconRequestPage implements OnInit { return modal.present() } - public async onOperationGroupUpdate(operationGroup: FullOperationGroup) { + public async onWrappedOperationUpdate(tezosWrappedOperation: TezosWrappedOperation) { if (!isOperationRequestOutput(this.request)) { return } - this.request = { ...this.request, operationDetails: operationGroup.contents } as OperationRequestOutput + this.request = { ...this.request, operationDetails: tezosWrappedOperation.contents } as OperationRequestOutput await this.operationRequest(this.request) const toast = await this.toastController.create({ message: `Updated Operation Details`, @@ -230,7 +229,7 @@ export class BeaconRequestPage implements OnInit { contents: request.operationDetails } - this.operationGroupPromise = this.operationProvider.operationGroupFromWrappedOperation( + this.wrappedOperationPromise = this.operationProvider.completeWrappedOperation( wrappedOperation, this.requestedNetwork !== undefined ? this.requestedNetwork : { type: NetworkType.MAINNET } ) @@ -262,23 +261,45 @@ export class BeaconRequestPage implements OnInit { const sourceAddress = (this.request as OperationRequestOutput).sourceAddress const wallets: WalletInfo[] | undefined = await this.walletService.getAllWallets() const wallet = wallets.find(w => w.address === sourceAddress) + const network = this.requestedNetwork !== undefined ? this.requestedNetwork : { type: NetworkType.MAINNET } try { - const dryRunPreview = await this.operationProvider.performDryRun( - wrappedOperation, - this.requestedNetwork !== undefined ? this.requestedNetwork : { type: NetworkType.MAINNET }, + const request = { + tezosWrappedOperation: wrappedOperation, + network, wallet - ) + } - this.openModal( - { - component: DryRunPreviewPage, - componentProps: { - preapplyResponse: dryRunPreview.preapplyResponse - } - }, - false - ) + const modalOptions = { + component: AddLedgerConnectionPage, + componentProps: { + request, + targetMethod: Action.DRY_RUN + } + } + const modal = await this.modalController.create(modalOptions) + + modal.present() + + modal + .onDidDismiss() + .then(async ({ data: dryRunResponse }) => { + const dryRunPreview = await this.operationProvider.performDryRun(dryRunResponse!.body, network) + + this.openModal( + { + component: DryRunPreviewPage, + componentProps: { + preapplyResponse: dryRunPreview, + network, + request: this.request, + signedTransaction: dryRunResponse!.signedTransaction + } + }, + false + ) + }) + .catch(error => console.error(error)) } catch (error) { console.error(error) this.openModal({ @@ -386,11 +407,12 @@ export class BeaconRequestPage implements OnInit { } } -type RequestOutput = | PermissionRequestOutput - | OperationRequestOutput - | SignPayloadRequestOutput - | BroadcastRequestOutput - | undefined +type RequestOutput = + | PermissionRequestOutput + | OperationRequestOutput + | SignPayloadRequestOutput + | BroadcastRequestOutput + | undefined function isOperationRequestOutput(request: RequestOutput): request is OperationRequestOutput { if (request === undefined) { diff --git a/src/app/pages/dry-run-preview/dry-run-preview.page.html b/src/app/pages/dry-run-preview/dry-run-preview.page.html index bea81d0..37c9c51 100644 --- a/src/app/pages/dry-run-preview/dry-run-preview.page.html +++ b/src/app/pages/dry-run-preview/dry-run-preview.page.html @@ -34,8 +34,11 @@
- + Close + + Confirm + diff --git a/src/app/pages/dry-run-preview/dry-run-preview.page.ts b/src/app/pages/dry-run-preview/dry-run-preview.page.ts index f6f6432..9998848 100644 --- a/src/app/pages/dry-run-preview/dry-run-preview.page.ts +++ b/src/app/pages/dry-run-preview/dry-run-preview.page.ts @@ -1,6 +1,10 @@ +import { BeaconMessageType, BroadcastRequestOutput, Network, OperationRequestOutput } from '@airgap/beacon-sdk' import { Component, Input, OnInit } from '@angular/core' import { ModalController } from '@ionic/angular' +import { ChromeMessagingService } from 'src/app/services/chrome-messaging.service' +import { Action } from 'src/extension/extension-client/Actions' import { PreapplyResponse, TezosGenericOperationError } from 'src/extension/tezos-types' + @Component({ selector: 'app-dry-run-preview', templateUrl: './dry-run-preview.page.html', @@ -10,10 +14,23 @@ export class DryRunPreviewPage implements OnInit { public errors: TezosGenericOperationError[] = [] public jsonString: string | undefined + + @Input() + public preapplyResponse!: PreapplyResponse[] + @Input() - public preapplyResponse: PreapplyResponse[] | undefined + public signedTransaction!: string - constructor(private readonly modalController: ModalController) {} + @Input() + public network!: Network + + @Input() + public request!: OperationRequestOutput + + constructor( + private readonly modalController: ModalController, + private readonly chromeMessagingService: ChromeMessagingService + ) {} ngOnInit(): void { if (this.preapplyResponse) { @@ -36,6 +53,22 @@ export class DryRunPreviewPage implements OnInit { } } + async confirm() { + const broadcastRequest: BroadcastRequestOutput = { + id: this.request.id, + senderId: this.request.senderId, + appMetadata: this.request.appMetadata, + type: BeaconMessageType.BroadcastRequest, + network: this.network, + signedTransaction: this.signedTransaction + } + this.chromeMessagingService.sendChromeMessage(Action.RESPONSE, { + request: broadcastRequest, + extras: undefined + }) + this.dismiss() + } + private flatten(arr: T[][]): T[] { return Array.prototype.concat.apply([], arr) } diff --git a/src/app/pipes/fee-converter/fee-converter.pipe.ts b/src/app/pipes/fee-converter/fee-converter.pipe.ts index c5cf6cf..5a2d204 100644 --- a/src/app/pipes/fee-converter/fee-converter.pipe.ts +++ b/src/app/pipes/fee-converter/fee-converter.pipe.ts @@ -7,7 +7,10 @@ import { BigNumber } from 'bignumber.js' name: 'feeConverter' }) export class FeeConverterPipe implements PipeTransform { - public transform(value: BigNumber | string | number, args: { protocolIdentifier: ProtocolSymbols, reverse?: boolean, appendSymbol?: boolean }): string { + public transform( + value: BigNumber | string | number, + args: { protocolIdentifier: ProtocolSymbols; reverse?: boolean; appendSymbol?: boolean } + ): string { const reverse = args.reverse !== undefined && args.reverse const appendSymbol = args.appendSymbol === undefined || args.appendSymbol if (BigNumber.isBigNumber(value)) { @@ -28,7 +31,7 @@ export class FeeConverterPipe implements PipeTransform { const amount = new BigNumber(value) const shiftDirection: number = !reverse ? -1 : 1 const fee = amount.shiftedBy(shiftDirection * protocol.feeDecimals) - + return fee.toFixed() + (appendSymbol ? ' ' + protocol.feeSymbol.toUpperCase() : '') } } diff --git a/src/app/services/chrome-messaging.service.ts b/src/app/services/chrome-messaging.service.ts index 07e7b18..ba37926 100644 --- a/src/app/services/chrome-messaging.service.ts +++ b/src/app/services/chrome-messaging.service.ts @@ -28,17 +28,18 @@ export class ChromeMessagingService { private updateWalletCallback: (() => Promise) | undefined private accountPresent: boolean = false - private readonly loader: Promise = this.loadingController.create({ - message: 'Preparing beacon message...' - }) + private readonly loader: Promise constructor( private readonly popupService: PopupService, private readonly ngZone: NgZone, - private readonly loadingController: LoadingController, + loadingController: LoadingController, private readonly modalController: ModalController, private readonly alertController: AlertController ) { + this.loader = loadingController.create({ + message: 'Preparing beacon message...' + }) chrome.runtime.sendMessage({ data: 'Handshake' }) // TODO: Remove and use Action.HANDSHAKE this.sendChromeMessage(Action.HANDSHAKE, undefined).catch(console.error) chrome.runtime.onMessage.addListener(async (message, _sender, _sendResponse) => { diff --git a/src/extension/AirGapSigner.ts b/src/extension/AirGapSigner.ts index 7b2ac44..ea2665e 100644 --- a/src/extension/AirGapSigner.ts +++ b/src/extension/AirGapSigner.ts @@ -4,13 +4,12 @@ import * as bs58check from '@airgap/coinlib-core/dependencies/src/bs58check-2.1. import { TezosWrappedOperation } from '@airgap/coinlib-core/protocols/tezos/types/TezosWrappedOperation' import { RawTezosTransaction } from '@airgap/coinlib-core/serializer/types' import Axios, { AxiosError, AxiosResponse } from 'axios' -import { WalletInfo, WalletType } from './extension-client/Actions' import { bridge } from './extension-client/ledger-bridge' import { Logger } from './extension-client/Logger' import { OperationProvider, Signer } from './extension-client/Signer' import { getProtocolForNetwork, getRpcUrlForNetwork } from './extension-client/utils' -import { DryRunResponse, DryRunSignatures, FullOperationGroup } from './tezos-types' +import { DryRunSignatures, PreapplyResponse } from './tezos-types' const logger: Logger = new Logger('AirGap Signer') @@ -35,48 +34,17 @@ export class AirGapOperationProvider implements OperationProvider { return forgedTx.binaryTransaction } - public async operationGroupFromWrappedOperation( + public async completeWrappedOperation( tezosWrappedOperation: TezosWrappedOperation, network: Network - ): Promise { + ): Promise { const { rpcUrl }: { rpcUrl: string; apiUrl: string } = await getRpcUrlForNetwork(network) - const { data: block }: AxiosResponse<{ chain_id: string }> = await Axios.get(`${rpcUrl}/chains/main/blocks/head`) const { data: branch } = await Axios.get(`${rpcUrl}/chains/main/blocks/head/hash`) - return { chain_id: block.chain_id, ...tezosWrappedOperation, branch: branch } + return { ...tezosWrappedOperation, branch: branch } } - public async performDryRun( - tezosWrappedOperation: TezosWrappedOperation, - network: Network, - wallet: WalletInfo | undefined - ): Promise { - const { rpcUrl }: { rpcUrl: string; apiUrl: string } = await getRpcUrlForNetwork(network) - const { data: block } = await Axios.get(`${rpcUrl}/chains/main/blocks/head`) - const forgedTx = await this.forgeWrappedOperation({ ...tezosWrappedOperation, branch: block.hash }, network) - let signatures: DryRunSignatures - if (!wallet) { - throw new Error('NO WALLET FOUND') - } - - if (wallet.type === WalletType.LOCAL_MNEMONIC) { - const localWallet: WalletInfo = wallet as WalletInfo - const signer: Signer = new LocalSigner() - signatures = await signer.generateDryRunSignatures({ binaryTransaction: forgedTx }, localWallet.info.mnemonic) - } else { - const signer: Signer = new LedgerSigner() - signatures = await signer.generateDryRunSignatures({ binaryTransaction: forgedTx }, wallet.derivationPath) - } - - const body = [ - { - protocol: block.protocol, - ...tezosWrappedOperation, - branch: block.hash, - signature: signatures.preapplySignature - } - ] - const preapplyResponse = await this.send(network, body, '/chains/main/blocks/head/helpers/preapply/operations') - return { preapplyResponse, signatures } + public async performDryRun(body: any, network: Network): Promise { + return this.send(network, [body], '/chains/main/blocks/head/helpers/preapply/operations') } public async broadcast(network: Network, signedTx: string): Promise { diff --git a/src/extension/extension-client/Actions.ts b/src/extension/extension-client/Actions.ts index 9c63cbf..1f01d66 100644 --- a/src/extension/extension-client/Actions.ts +++ b/src/extension/extension-client/Actions.ts @@ -1,4 +1,6 @@ import { ExtendedP2PPairingResponse, Network, P2PPairingRequest, PermissionInfo } from '@airgap/beacon-sdk' +import { TezosWrappedOperation } from '@airgap/coinlib-core' +import { TezosOperation } from '@airgap/coinlib-core/protocols/tezos/types/operations/TezosOperation' export enum WalletType { P2P = 'P2P', @@ -62,7 +64,9 @@ export interface ActionInputTypesMap { [Action.LEDGER_INIT]: undefined [Action.BEACON_ID_GET]: undefined [Action.RESPONSE]: { request: unknown; extras: unknown } - [Action.DRY_RUN]: undefined + [Action.DRY_RUN]: { + request: { tezosWrappedOperation: TezosWrappedOperation; network: Network; wallet: WalletInfo | undefined } + } } export interface ActionOutputTypesMap { @@ -84,7 +88,15 @@ export interface ActionOutputTypesMap { [Action.LEDGER_INIT]: { publicKey: string; address: string } [Action.BEACON_ID_GET]: { id: string } [Action.RESPONSE]: { error?: unknown } - [Action.DRY_RUN]: { dryRunPreview: string } + [Action.DRY_RUN]: { + body: { + protocol: string + contents: TezosOperation[] + branch: string + signature: string + } + signedTransaction: string + } } export interface ExtensionMessageInputPayload { diff --git a/src/extension/extension-client/action-handler/ActionMessageHandler.ts b/src/extension/extension-client/action-handler/ActionMessageHandler.ts index c41ea34..9fa0ae8 100644 --- a/src/extension/extension-client/action-handler/ActionMessageHandler.ts +++ b/src/extension/extension-client/action-handler/ActionMessageHandler.ts @@ -21,6 +21,7 @@ import { walletAddAction } from './wallet-add-action' import { openFullscreen } from './open-fullscreen' import { walletDeleteAction } from './wallet-delete-action' import { walletsGetAction } from './wallets-get-action' +import { dryRunAction } from './dry-run-action' const logger: Logger = new Logger('action-message-handler.ts') @@ -60,7 +61,7 @@ export class ActionMessageHandler { [Action.LEDGER_INIT]: ledgerInitAction(logger), [Action.BEACON_ID_GET]: beaconIdGetAction(logger), [Action.RESPONSE]: responseAction(logger), - [Action.DRY_RUN]: responseAction(logger) + [Action.DRY_RUN]: dryRunAction(logger) } public async getHandler(action: Action): Promise> { diff --git a/src/extension/extension-client/action-handler/dry-run-action.ts b/src/extension/extension-client/action-handler/dry-run-action.ts new file mode 100644 index 0000000..f0ae928 --- /dev/null +++ b/src/extension/extension-client/action-handler/dry-run-action.ts @@ -0,0 +1,58 @@ +import { Network } from '@airgap/beacon-sdk' +import { RawTezosTransaction, TezosProtocol } from '@airgap/coinlib-core' +import { TezosWrappedOperation } from '@airgap/coinlib-core/protocols/tezos/types/TezosWrappedOperation' +import Axios from 'axios' +import { LedgerSigner, LocalSigner } from 'src/extension/AirGapSigner' +import { DryRunSignatures } from 'src/extension/tezos-types' + +import { Action, WalletInfo, WalletType } from '../Actions' +import { Logger } from '../Logger' +import { Signer } from '../Signer' +import { getProtocolForNetwork, getRpcUrlForNetwork } from '../utils' + +import { ActionContext, ActionHandlerFunction } from './ActionMessageHandler' + +export const dryRunAction: (logger: Logger) => ActionHandlerFunction = ( + logger: Logger +): ActionHandlerFunction => async (context: ActionContext): Promise => { + logger.log('dryRunAction') + const tezosWrappedOperation = context.data.data.request.tezosWrappedOperation + const network = context.data.data.request.network + const wallet = context.data.data.request.wallet + const { rpcUrl }: { rpcUrl: string; apiUrl: string } = await getRpcUrlForNetwork(network) + const { data: block } = await Axios.get(`${rpcUrl}/chains/main/blocks/head`) + const forgedTx = await forgeWrappedOperation({ ...tezosWrappedOperation, branch: block.hash }, network) + let signatures: DryRunSignatures + if (!wallet) { + throw new Error('NO WALLET FOUND') + } + + if (wallet.type === WalletType.LOCAL_MNEMONIC) { + const localWallet: WalletInfo = wallet as WalletInfo + const signer: Signer = new LocalSigner() + signatures = await signer.generateDryRunSignatures({ binaryTransaction: forgedTx }, localWallet.info.mnemonic) + } else { + const signer: Signer = new LedgerSigner() + signatures = await signer.generateDryRunSignatures({ binaryTransaction: forgedTx }, wallet.derivationPath) + } + + const body = { + protocol: block.protocol, + contents: tezosWrappedOperation.contents, + branch: block.hash, + signature: signatures.preapplySignature + } + + context.sendResponse({ data: { body, signedTransaction: signatures.signedTransaction } }) +} + +export const forgeWrappedOperation = async ( + wrappedOperation: TezosWrappedOperation, + network: Network +): Promise => { + const protocol: TezosProtocol = await getProtocolForNetwork(network) + + const forgedTx: RawTezosTransaction = await protocol.forgeAndWrapOperations(wrappedOperation) + + return forgedTx.binaryTransaction +} diff --git a/src/extension/tezos-types.ts b/src/extension/tezos-types.ts index 9ad5d81..dd258cd 100644 --- a/src/extension/tezos-types.ts +++ b/src/extension/tezos-types.ts @@ -1,5 +1,3 @@ -import { TezosWrappedOperation } from '@airgap/coinlib-core' - export interface TezosGenericOperationError { kind: string id: string @@ -31,10 +29,6 @@ export interface DryRunResponse { signatures: DryRunSignatures } -export interface FullOperationGroup extends TezosWrappedOperation { - chain_id: string -} - export interface DryRunSignatures { preapplySignature: string signedTransaction: string