Skip to content

Commit

Permalink
PSBT Support Added for Blockcore Wallet (#350)
Browse files Browse the repository at this point in the history
* PSBT signing added

* Fixed workflow error

* Added testnet3

* PSBT support completed

* Update README.md

Added link for PSBT Test guide
  • Loading branch information
tirth2004 authored Sep 9, 2024
1 parent 5246be8 commit c98ae36
Show file tree
Hide file tree
Showing 19 changed files with 257 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
- main
workflow_dispatch:

permissions:
contents: write

jobs:
build:
name: Build, Test and Release
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,8 @@ The `lists` is a git submodule and to update to latest:
```sh
git submodule update --remote --merge
```

## PSBT Test guide

To test the PSBT signing, there are few pre-built vectors, along with wallet configuration. You can refer to this document for the details:
https://docs.google.com/document/d/1we2a26ezo914LcbEMVe4V8Fwl2zK2194si_6yL4Wd8E/edit?usp=sharing
3 changes: 3 additions & 0 deletions angular/src/app/action/sign-psbt/signpsbt.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.signing-content {
height: 120px;
}
5 changes: 5 additions & 0 deletions angular/src/app/action/sign-psbt/signpsbt.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<br />{{ "Action.AttestContentDescription" | translate }}: <br /><br />
<mat-form-field class="input-full-width" appearance="outline">
<mat-label>{{ "Action.SigningPSBTLabel" | translate }}</mat-label>
<textarea class="signing-content" readonly="readonly" matInput [ngModel]="content"></textarea>
</mat-form-field>
25 changes: 25 additions & 0 deletions angular/src/app/action/sign-psbt/signpsbt.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { UIState } from '../../services';
import { ActionService } from 'src/app/services/action.service';

@Component({
selector: 'app-sign-psbt',
templateUrl: './signpsbt.component.html',
styleUrls: ['./signpsbt.component.css'],
})
export class ActionSignPsbtComponent implements OnInit, OnDestroy {
content: string;

constructor(public uiState: UIState, public actionService: ActionService) {
this.actionService.consentType = 'regular';
}

ngOnDestroy(): void { }

ngOnInit(): void {
// The content to be signed will be a PSBT represented in a string

this.content = this.uiState.action.content;

}
}
12 changes: 10 additions & 2 deletions angular/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { ExchangeComponent } from './exchange/exchange.component';
import { BiometricComponent } from './settings/biometric/biometric.component';
import { ActionSignVerifiableCredentialComponent } from './action/sign-credential/sign.component';
import { ActionSignMessageComponent } from './action/sign-message/sign.component';
import { ActionSignPsbtComponent } from './action/sign-psbt/signpsbt.component';
import { ActionVaultSetupComponent } from './action/vault-setup/vault-setup.component';
import { PaymentComponent } from './account/payment/payment.component';
import { DebuggerComponent } from './settings/debugger/debugger.component';
Expand Down Expand Up @@ -421,6 +422,13 @@ const routes: Routes = [
data: LoadingResolverService,
},
},
{
path: 'signPsbt',
component: ActionSignPsbtComponent,
resolve: {
data: LoadingResolverService,
},
},
{
path: 'wallets',
component: ActionWalletsComponent,
Expand Down Expand Up @@ -498,7 +506,7 @@ const routes: Routes = [
data: LoadingResolverService,
},
},

// {
// path: 'signVerifiableCredential',
// component: ActionSignVerifiableCredentialComponent,
Expand Down Expand Up @@ -569,4 +577,4 @@ const routes: Routes = [
imports: [RouterModule.forRoot(routes, { enableTracing: false, useHash: true })],
exports: [RouterModule],
})
export class AppRoutingModule {}
export class AppRoutingModule { }
4 changes: 3 additions & 1 deletion angular/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import { ExchangeComponent } from './exchange/exchange.component';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { StandardTokenStore } from '../shared/store/standard-token-store';
import { ActionSignMessageComponent } from './action/sign-message/sign.component';
import { ActionSignPsbtComponent } from './action/sign-psbt/signpsbt.component';
import { ActionSignVerifiableCredentialComponent } from './action/sign-credential/sign.component';
import { RuntimeService } from 'src/shared/runtime.service';
import { StorageService } from 'src/shared/storage.service';
Expand Down Expand Up @@ -199,6 +200,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
ContactsViewComponent,
ExchangeComponent,
ActionSignMessageComponent,
ActionSignPsbtComponent,
ActionSignVerifiableCredentialComponent,
ActionVaultSetupComponent,
PaymentComponent,
Expand Down Expand Up @@ -338,4 +340,4 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
],
bootstrap: [AppComponent],
})
export class AppModule {}
export class AppModule { }
18 changes: 17 additions & 1 deletion angular/src/app/services/wallet-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,17 @@ export class WalletManager {
async addAccount(account: Account, wallet: Wallet, runIndexIfRestored = true) {
try {
const network = this.getNetwork(account.networkType);

//Un comment the following part to get all the details for creating a test PSBT.

// Get the secret seed.
// const masterSeedBase64 = this.secure.get(wallet.id);
// console.log("Wallet and the wallet id: ", wallet, wallet.id)
// const masterSeed = Buffer.from(masterSeedBase64, 'base64');
// const hdNode = bip32.fromSeed(masterSeed, network);
// console.log("HD Node: ", hdNode)
// const xPrivKey = hdNode.toBase58();
// console.log("Extended Private Key (XPriv):", xPrivKey);

if (!account.prv) {
// First derive the xpub and store that on the account.
Expand All @@ -732,8 +743,13 @@ export class WalletManager {
const masterSeed = Buffer.from(masterSeedBase64, 'base64');
const masterNode = HDKey.fromMasterSeed(masterSeed, network.bip32);
const accountNode = masterNode.derive(`m/${account.purpose}'/${account.network}'/${account.index}'`);

// console.log("Account node: ", accountNode);
account.xpub = accountNode.publicExtendedKey;

// Again, uncomment the following logs, to access information you would need to create a test PSBT

// console.log("Extended public key of new account: ", account.xpub);
// console.log("Path: ", account.purpose, account.network, account.index)
}

// Add account to the wallet and persist.
Expand Down
4 changes: 3 additions & 1 deletion angular/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@
"vc.request": "VC Request",
"vaultSetup": "Vault Setup",
"signMessage": "Sign Message",
"signPsbt": "Sign PSBT",
"IdentityDID": "Identity (DID)",
"SigningAccount": "Signing Account",
"DerivationPath": "Derivation Path",
Expand All @@ -391,6 +392,7 @@
"DecryptionContentLabel": "Decryption Content",
"SignAndClose": "Sign and Close",
"ExitAction": "Exit Action",
"SigningPSBTLabel": "Raw PSBT",
"ActionIdentity": "Action: Identity",
"SigningContent": "The website is requesting you to generate a DID Document.",
"Login": "Login",
Expand Down Expand Up @@ -463,4 +465,4 @@
"portuguese": "Portuguese",
"spanish": "Spanish",
"russian": "Russian"
}
}
19 changes: 16 additions & 3 deletions angular/src/shared/background-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class BackgroundManager {

// Create the master node.
const masterNode = HDKey.fromMasterSeed(masterSeed, network.bip32);

// based on BCIP3 we allow to derive a key only
// under the wallet path and it must be hardened keys.
var node = masterNode.derive(`m/3'/${account.network}'/${path}`);
Expand Down Expand Up @@ -178,6 +178,11 @@ export class BackgroundManager {
return { network, node };
}

async getMasterSeedBase64(walletId: string) {
const masterSeedBase64 = this.sharedManager.getPrivateKey(walletId);
return masterSeedBase64;
}

async updateNetworkStatus(instance: string) {
const walletStore = new WalletStore();
await walletStore.load();
Expand Down Expand Up @@ -214,7 +219,9 @@ export class BackgroundManager {
private async updateAll(accounts: Account[], networkLoader: NetworkLoader, settingStore: SettingStore) {
// Make only a unique list of accounts:
const uniqueAccounts = accounts.filter((value, index, self) => self.map((x) => x.networkType).indexOf(value.networkType) == index);

// for (let i = 0; i < uniqueAccounts.length; i++) {
// console.log("Account " + i + ": ", uniqueAccounts[i]);
// }
const settings = settingStore.get();

// Load all services from selected nameservers:
Expand All @@ -224,7 +231,9 @@ export class BackgroundManager {
const account = accounts[i];
const network = networkLoader.getNetwork(account.networkType);
const indexerUrls = networkLoader.getServers(network.id, settings.server, settings.indexer) || [];

// console.log("Network: ", network);
// console.log("Indexer URL: ", indexerUrls);
//TODO: No Indexer URL for bitcoin testnet.
if (indexerUrls == null && account.type != 'identity') {
console.warn(`Invalid configuration of servers. There are no servers registered for network of type ${network.id}.`);
continue;
Expand All @@ -248,8 +257,11 @@ export class BackgroundManager {
timeout: 3000,
});

// console.log("URL used to fetch: ", indexerUrl);
// console.debug("Response: ", response);
if (response.ok) {
const data = await response.json();
// console.debug("Data: ", data);
if (data.error) {
networkStatus = {
domain,
Expand Down Expand Up @@ -297,6 +309,7 @@ export class BackgroundManager {
};
}
} catch (error: any) {

console.log('Error on Network Status Service:', error);

if (error.response) {
Expand Down
17 changes: 16 additions & 1 deletion angular/src/shared/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Account } from './interfaces';
import { BTC, CITY, CRS, CY, IDENTITY, NOSTR, RSC, SBC, STRAX, TCRS, TSTRAX, X42 , IMPLX, MOL, KEY, XRC, SERF} from './networks';
import { BITCOIN_TESTNET, BTC, CITY, CRS, CY, IDENTITY, NOSTR, RSC, SBC, STRAX, TCRS, TSTRAX, X42, IMPLX, MOL, KEY, XRC, SERF } from './networks';
import { JWK } from './networks/JWK';
const { v4: uuidv4 } = require('uuid');

Expand All @@ -24,6 +24,7 @@ export class Defaults {
networks.push(new X42());
networks.push(new XRC());
networks.push(new SERF());
networks.push(new BITCOIN_TESTNET());
return networks;
}

Expand Down Expand Up @@ -284,6 +285,20 @@ export class Defaults {
purposeAddress: 44,
icon: 'account_circle',
},
{
identifier: uuidv4(),
index: 0,
networkType: 'BITCOIN_TESTNET',
mode: 'normal',
selected: false,
name: 'Bitcoin Testnet',
type: 'coin',
network: 1,
purpose: 44,
purposeAddress: 44,
icon: 'paid',
}

];
break;
case 'freecity':
Expand Down
3 changes: 3 additions & 0 deletions angular/src/shared/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PaymentSignHandler } from './payment-sign-handler';
// import { SignVerifiableCredentialHandler } from './sign-credential-handler';
// import { SignHandler } from './sign-handler';
import { SignMessageHandler } from './sign-message-handler';
import { SignPsbtHandler } from './sign-psbt-handler';
import { VcRequestHandler } from './vc-request-handler';
import { AccountBalanceHandler } from './account-balance-handler';
import { WalletsHandler } from './wallets-handler';
Expand All @@ -29,6 +30,8 @@ export class Handlers {
// return new SignHandler();
case 'signMessage': // Signing using a message prefix specific for the network the account belongs to. More secure and cannot be used to sign transactions.
return new SignMessageHandler(backgroundManager);
case 'signPsbt': // Signing of a PSBT (Partially Signed Bitcoin Transaction).
return new SignPsbtHandler(backgroundManager);
case 'payment':
return new PaymentHandler(backgroundManager);
case 'payment.sign':
Expand Down
86 changes: 86 additions & 0 deletions angular/src/shared/handlers/sign-psbt-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { BackgroundManager } from '../background-manager';
import { ActionPrepareResult, ActionResponse, Permission } from '../interfaces';
import { ActionHandler, ActionState } from './action-handler';
import { HDKey } from '@scure/bip32';
import { Network } from '../networks';
import { Psbt } from '@blockcore/blockcore-js';
import BIP32Factory from 'bip32';
import ecc from '@bitcoinerlab/secp256k1';
const Bip32 = BIP32Factory(ecc);
import { bip32 } from '../noble-ecc-wrapper';

// const testHDKey = "tprv8ZgxMBicQKsPf5D3jKXvvvsRi3RMZvE8g98752W3KeCUeggbyf8HQ3BJjppWRrHVPhkgKefZZD8x1jCQSHkoSLaagVNWPBfgTtJVhaRewR5";

export class SignPsbtHandler implements ActionHandler {
action = ['signPsbt'];

constructor(private backgroundManager: BackgroundManager) { }

private hexToBase64(hex: string): string {
const buffer = Buffer.from(hex, 'hex');
return buffer.toString('base64');
}

async signPsbt(network: Network, content: string, xPrivKey: string): Promise<string> {
// console.debug("PSBT Handler has been called");
var hexPsbt = content;
const psbtBase64 = this.hexToBase64(hexPsbt);
// const network = {
// messagePrefix: "\u0018Bitcoin Signed Message:\n",
// bech32: "tb",
// bip32: {
// public: 70617039,
// private: 70615956,
// },
// pubKeyHash: 111,
// scriptHash: 196,
// wif: 239,
// };
const psbt = Psbt.fromBase64(psbtBase64, {
network,
});
// console.log("psbt: ", psbt);
const hdKeyPair = Bip32.fromBase58(xPrivKey, network);
// console.log("HD key Pair: ", hdKeyPair);
const SignedPsbt = psbt.signAllInputsHD(hdKeyPair);
// console.log(SignedPsbt);

return SignedPsbt.toBase64();
}


async prepare(state: ActionState): Promise<ActionPrepareResult> {
if (!state.message || !state.message.request || !state.message.request.params || !state.message.request.params[0] || !state.message.request.params[0].message) {
throw Error('The params must include a single entry that has a message field.');
}

return {
content: state.message.request.params[0].message,
consent: true,
};
}

async execute(state: ActionState, permission: Permission): Promise<ActionResponse> {
// Get the private key
const { node, network } = await this.backgroundManager.getKey(permission.walletId, permission.accountId, permission.keyId);
// console.log("HD Key pair in Blockcore wallet: ", node);
const masterSeedBase64 = await this.backgroundManager.getMasterSeedBase64(permission.walletId)
const masterSeed = Buffer.from(masterSeedBase64, 'base64');
const hdNode = bip32.fromSeed(masterSeed, network);
const xPrivKey = hdNode.toBase58();

if (state.content) {
let contentText = state.content;

if (typeof state.content !== 'string') {
contentText = JSON.stringify(state.content);
}

let signedData = await this.signPsbt(network, contentText as string, xPrivKey);

return { key: permission.key, walletId: permission.walletId, accountId: permission.accountId, response: { signature: signedData, content: state.content }, network: network.id };
} else {
return { key: '', response: null, network: network.id };
}
}
}
Loading

0 comments on commit c98ae36

Please sign in to comment.