Skip to content

Commit

Permalink
Merge branch 'feat/segwit-psbt' into 'develop'
Browse files Browse the repository at this point in the history
Feat/segwit psbt

See merge request papers/airgap/airgap-vault!257
  • Loading branch information
godenzim committed Oct 7, 2021
2 parents 4163c05 + 3afb546 commit 373b0a6
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,6 @@ publish_ios:
script:
- export DEVELOPER_DIR=$XCODE_PATH
- xcodebuild -exportArchive -archivePath ios/airgap-vault-$VERSION-$CI_PIPELINE_ID.xcarchive -exportOptionsPlist exportOptions.plist -exportPath ios/ -allowProvisioningUpdates
- xcrun altool --upload-app -f ios/App.ipa -u $IOS_USERNAME -p $IOS_PASSWORD
- xcrun altool --upload-app -f ios/App.ipa -t ios -u $IOS_USERNAME -p $IOS_PASSWORD
tags:
- ios
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
"apply-diagnostic-modules": "node apply-diagnostic-modules.js"
},
"dependencies": {
"@airgap/angular-core": "0.0.19-beta.0",
"@airgap/angular-ngrx": "0.0.19-beta.0",
"@airgap/coinlib-core": "0.11.14",
"@airgap/angular-core": "0.0.19",
"@airgap/angular-ngrx": "0.0.19",
"@airgap/coinlib-core": "0.12.0",
"@airgap/sapling-wasm": "0.0.6",
"@angular/common": "^11.2.9",
"@angular/core": "^11.2.9",
Expand All @@ -57,6 +57,7 @@
"@ionic/angular": "5.6.4",
"@ionic/core": "5.6.4",
"@ionic/storage": "^2.2.0",
"@keystonehq/bc-ur-registry": "0.4.4",
"@ngraveio/bc-ur": "^1.1.3",
"@ngrx/component-store": "^10.1.2",
"@ngrx/effects": "^10.1.2",
Expand Down
3 changes: 3 additions & 0 deletions src/app/components/components.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SecretOptionItemComponent } from './secret-option-item/secret-option-it
import { SignedTransactionComponent } from './signed-transaction/signed-transaction.component'
import { TouchEntropyComponent } from './touch-entropy/touch-entropy.component'
import { TraceInputDirective } from './trace-input/trace-input.directive'
import { TransactionWarningComponent } from './transaction-warning/transaction-warning.component'
import { TransactionComponent } from './transaction/transaction.component'
import { VerifyKeyComponent } from './verify-key/verify-key.component'

Expand All @@ -30,6 +31,7 @@ import { VerifyKeyComponent } from './verify-key/verify-key.component'
SecretItemComponent,
SignedTransactionComponent,
TransactionComponent,
TransactionWarningComponent,
TouchEntropyComponent,
TraceInputDirective,
VerifyKeyComponent,
Expand All @@ -46,6 +48,7 @@ import { VerifyKeyComponent } from './verify-key/verify-key.component'
SecretItemComponent,
SignedTransactionComponent,
TransactionComponent,
TransactionWarningComponent,
TouchEntropyComponent,
TraceInputDirective,
VerifyKeyComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ <h5>{{ aggregatedInfo.totalFees.toFixed() | feeConverter: { protocol: airGapTxs[
</ion-row>
</ng-container>

<airgap-from-to *ngFor="let airGapTx of airGapTxs" [transaction]="airGapTx"></airgap-from-to>
<ng-container *ngFor="let airGapTx of airGapTxs">
<airgap-transaction-warning [transaction]="airGapTx"></airgap-transaction-warning>
<airgap-from-to class="ion-padding-horizontal" [transaction]="airGapTx"></airgap-from-to>
</ng-container>

<ng-container *ngIf="fallbackActivated">
<ion-row>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<ion-row *ngIf="warnings && warnings.length > 0" class="ion-align-items-center ion-padding-top">
<ion-col *ngFor="let warning of warnings" size="12">
<ion-item lines="none" [detail]="false" class="ion-margin-vertical" [color]="warning.color">
<ion-icon *ngIf="warning.icon" [name]="warning.icon" slot="start" color="dark"></ion-icon>
<ion-label color="dark" class="ion-text-wrap">
<h3>
<strong>{{ warning.title | translate }}</strong>
</h3>
<p>{{ warning.description | translate }}</p>
<!-- <div *ngIf="warning.actions">
<ion-button color="light" *ngFor="let action of warning.actions" (click)="doAction(action)">
{{ action.text | translate }}
</ion-button>
</div> -->
</ion-label>
</ion-item>
</ion-col>
</ion-row>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'
import { IonicModule } from '@ionic/angular'

import { TransactionWarningComponent } from './transaction-warning.component'

describe('TransactionWarningComponent', () => {
let component: TransactionWarningComponent
let fixture: ComponentFixture<TransactionWarningComponent>

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [TransactionWarningComponent],
imports: [IonicModule.forRoot()]
}).compileComponents()

fixture = TestBed.createComponent(TransactionWarningComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
)

it('should create', () => {
expect(component).toBeTruthy()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { IAirGapTransaction } from '@airgap/coinlib-core'
import { AirGapTransactionWarning, AirGapTransactionWarningType } from '@airgap/coinlib-core/interfaces/IAirGapTransaction'
import { Component, Input } from '@angular/core'

@Component({
selector: 'airgap-transaction-warning',
templateUrl: './transaction-warning.component.html',
styleUrls: ['./transaction-warning.component.scss']
})
export class TransactionWarningComponent {
@Input()
public set transaction(value: IAirGapTransaction | undefined) {
this._transaction = value
if (value) {
this.warnings = value.warnings.map((warning) => {
return {
...warning,
color:
warning.type === AirGapTransactionWarningType.SUCCESS
? 'success'
: warning.type === AirGapTransactionWarningType.NOTE
? 'light'
: warning.type === AirGapTransactionWarningType.WARNING
? 'warning'
: warning.type === AirGapTransactionWarningType.ERROR
? 'danger'
: 'primary',
icon: warning.icon
? warning.icon
: warning.type === AirGapTransactionWarningType.SUCCESS
? 'checkmark-circle-outline'
: warning.type === AirGapTransactionWarningType.NOTE
? 'information-circle-outline'
: warning.type === AirGapTransactionWarningType.WARNING
? 'warning'
: warning.type === AirGapTransactionWarningType.ERROR
? 'warning'
: 'warning'
}
})
}
}

public get transaction(): IAirGapTransaction | undefined {
return this._transaction
}

public _transaction: IAirGapTransaction | undefined

public warnings: (AirGapTransactionWarning & { color: string })[] | undefined

constructor() {}

// doAction(warning: AirGapTransactionWarning) {
// // TODO: Use link page
// console.log(warning)
// }
}
5 changes: 4 additions & 1 deletion src/app/components/transaction/transaction.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ <h5>{{ aggregatedDetails.totalFees.toFixed() | feeConverter: { protocol: protoco
</ion-row>
</ng-container>

<airgap-from-to *ngFor="let airGapTx of airGapTxs" [transaction]="airGapTx"></airgap-from-to>
<ng-container *ngFor="let airGapTx of airGapTxs">
<airgap-transaction-warning [transaction]="airGapTx"></airgap-transaction-warning>
<airgap-from-to class="ion-padding-horizontal" [transaction]="airGapTx"></airgap-from-to>
</ng-container>
</ng-container>
1 change: 1 addition & 0 deletions src/app/pages/account-share/account-share.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ <h3 class="ion-padding-bottom" [innerHTML]="'wallet-share.heading' | translate">
<ion-toggle [checked]="displayRawData" (ionChange)="displayRawData = !displayRawData" slot="end"></ion-toggle>
</ion-item>
<pre *ngIf="displayRawData" class="ion-no-margin">{{ interactionUrl | json }}</pre>
<ion-button *ngIf="displayRawData" (click)="copyToClipboard()">Copy data to clipboard</ion-button>
</ion-col>
</ion-row>

Expand Down
7 changes: 6 additions & 1 deletion src/app/pages/account-share/account-share.page.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ClipboardService } from '@airgap/angular-core'
import { IACMessageDefinitionObjectV3 } from '@airgap/coinlib-core'
import { Component } from '@angular/core'

Expand All @@ -16,11 +17,15 @@ export class AccountSharePage {

displayRawData: boolean = false

constructor(private readonly navigationService: NavigationService) {
constructor(private readonly navigationService: NavigationService, private readonly clipboardService: ClipboardService) {
this.interactionUrl = this.navigationService.getState().interactionUrl
}

public done(): void {
this.navigationService.routeToAccountsTab().catch(handleErrorLocal(ErrorCategory.IONIC_NAVIGATION))
}

public copyToClipboard(): void {
this.clipboardService.copyAndShowToast(JSON.stringify(this.interactionUrl))
}
}
28 changes: 28 additions & 0 deletions src/app/services/iac/iac.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
AirGapWalletStatus,
IACMessageDefinitionObjectV3,
IACMessageType,
MainProtocolSymbols,
MessageSignRequest,
UnsignedTransaction
} from '@airgap/coinlib-core'
Expand All @@ -24,6 +25,7 @@ import { ErrorCategory, handleErrorLocal } from '../error-handler/error-handler.
import { InteractionOperationType, InteractionService } from '../interaction/interaction.service'
import { NavigationService } from '../navigation/navigation.service'
import { SecretsService } from '../secrets/secrets.service'
import * as bitcoinJS from 'bitcoinjs-lib'

@Injectable({
providedIn: 'root'
Expand Down Expand Up @@ -68,6 +70,32 @@ export class IACService extends BaseIACService {
signTransactionRequest.protocol
)

if (!correctWallet && signTransactionRequest.protocol === MainProtocolSymbols.BTC_SEGWIT) {
const decodedPSBT = bitcoinJS.Psbt.fromHex(unsignedTransaction.transaction)
for (const input of decodedPSBT.data.inputs) {
for (const derivation of input.bip32Derivation) {
const masterFingerprint = derivation.masterFingerprint.toString('hex')

correctWallet = this.secretsService.findWalletByFingerprintDerivationPathAndProtocolIdentifier(
masterFingerprint,
signTransactionRequest.protocol,
derivation.path,
derivation.pubkey
)
if (correctWallet) {
break
}
}
if (correctWallet) {
break
}
}

if (correctWallet && !unsignedTransaction.publicKey) {
unsignedTransaction.publicKey = correctWallet.publicKey // PSBT txs don't include a public key, so we need to set it
}
}

if (correctWallet) {
await this.activateWallet(correctWallet)
}
Expand Down
70 changes: 70 additions & 0 deletions src/app/services/secrets/secrets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,33 @@ import { ErrorCategory, handleErrorLocal } from '../error-handler/error-handler.
import { NavigationService } from '../navigation/navigation.service'
import { SecureStorage, SecureStorageService } from '../secure-storage/secure-storage.service'
import { VaultStorageKey, VaultStorageService } from '../storage/storage.service'
import * as bitcoinJS from 'bitcoinjs-lib'

import * as bs58check from 'bs58check'

class ExtendedPublicKey {
private readonly rawKey: Buffer
constructor(extendedPublicKey: string) {
this.rawKey = bs58check.decode(extendedPublicKey).slice(4)
}

toXpub() {
return this.addPrefix('0488b21e')
}

toYPub() {
return this.addPrefix('049d7cb2')
}

toZPub() {
return this.addPrefix('04b24746')
}

private addPrefix(prefix: string) {
const data = Buffer.concat([Buffer.from(prefix, 'hex'), this.rawKey])
return bs58check.encode(data)
}
}

interface AddWalletConifg {
protocolIdentifier: ProtocolSymbols
Expand Down Expand Up @@ -192,6 +219,17 @@ export class SecretsService {
})
}

public findByFingerprint(fingerprint: string): MnemonicSecret | undefined {
for (const secret of this.secretsList) {
const foundWallet: AirGapWallet | undefined = secret.wallets.find((wallet: AirGapWallet) => wallet.masterFingerprint === fingerprint)
if (foundWallet !== undefined) {
return secret
}
}

return undefined
}

public findByPublicKey(pubKey: string): MnemonicSecret | undefined {
for (const secret of this.secretsList) {
const foundWallet: AirGapWallet | undefined = secret.wallets.find((wallet: AirGapWallet) => wallet.publicKey === pubKey)
Expand Down Expand Up @@ -236,6 +274,38 @@ export class SecretsService {
return foundWallet
}

public findWalletByFingerprintDerivationPathAndProtocolIdentifier(
fingerprint: string,
protocolIdentifier: ProtocolSymbols,
derivationPath: string,
publicKey: Buffer
): AirGapWallet | undefined {
const secret: MnemonicSecret | undefined = this.findByFingerprint(fingerprint)
if (!secret) {
return undefined
}

const foundWallet: AirGapWallet | undefined = secret.wallets.find((wallet: AirGapWallet) => {
const match = wallet.masterFingerprint === fingerprint && wallet.protocol.identifier === protocolIdentifier
if (match) {
if (!derivationPath.startsWith(wallet.derivationPath)) {
return false
}

// This uses the same logic to find child key as "sign" method in the BitcoinSegwitProtocol
const bip32PK = bitcoinJS.bip32.fromBase58(new ExtendedPublicKey(wallet.publicKey).toXpub())
const cutoffFrom = derivationPath.lastIndexOf("'") || derivationPath.lastIndexOf('h')
const childPath = derivationPath.substr(cutoffFrom + 2)
const walletPublicKey = bip32PK.derivePath(childPath).publicKey

return publicKey.equals(walletPublicKey)
}
return false
})

return foundWallet
}

public findBaseWalletByPublicKeyAndProtocolIdentifier(pubKey: string, protocolIdentifier: ProtocolSymbols): AirGapWallet | undefined {
const secret: MnemonicSecret | undefined = this.findByPublicKey(pubKey)
if (!secret) {
Expand Down
Loading

0 comments on commit 373b0a6

Please sign in to comment.