Skip to content

Commit

Permalink
Merge branch 'develop' into denys/eng-3617-speed-up-stacks-transactio…
Browse files Browse the repository at this point in the history
…ns-on-mobile
  • Loading branch information
dhriaznov authored Feb 5, 2024
2 parents 945e9fa + 3925fe3 commit fb5e1cf
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 90 deletions.
6 changes: 6 additions & 0 deletions api/xverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,9 @@ export async function getAppConfig(network: NetworkType) {
const appConfig = await axios.get(appConfigUrl);
return appConfig;
}

export async function getSpamTokensList(network: NetworkType) {
const spamTokensUrl = `${XVERSE_API_BASE_URL(network)}/v1/spam-tokens`;
const spamTokens = await axios.get(spamTokensUrl);
return spamTokens.data;
}
1 change: 1 addition & 0 deletions connect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './auth';
export * from './bip322Signature';
export * from './signature';
export * from './transactionRequest';
export * from './manifest';
40 changes: 40 additions & 0 deletions connect/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import axios from 'axios';

export interface WebManifest {
icons?: { src: string; sizes?: string }[];
}

// Fetch the web manifest
export async function getManifestFile(url: string) {
try {
const response = await axios.get<WebManifest>(`${url}/manifest.json`);
return response.data;
} catch (err) {
const response = await axios.get<WebManifest>(`${url}/manifest.webmanifest`);
if (response.data) {
return response.data;
}
}
}

export async function getAppIconFromWebManifest(url: string): Promise<string> {
// Validate URL format
if (!/^https?:\/\/.*/.test(url)) {
throw new Error('Invalid URL format');
}
const { origin } = new URL(url);
const manifest = await getManifestFile(origin);

if (manifest) {
// Extract the app icons' URLs

const firstIconSrc = manifest?.icons?.find((icon) => icon.sizes === '48x48')?.src?.replace(/^\/+/, '');

if (!firstIconSrc) {
return '';
}
return `${origin}/${firstIconSrc}`;
} else {
return '';
}
}
88 changes: 1 addition & 87 deletions ledger/btc.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { getAddressInfo } from 'bitcoin-address-validation';
import { Psbt, Transaction } from 'bitcoinjs-lib';
import { Psbt } from 'bitcoinjs-lib';
import { AppClient, DefaultWalletPolicy } from 'ledger-bitcoin';
import { encode } from 'varuint-bitcoin';
import EsploraApiProvider from '../api/esplora/esploraAPiProvider';
import { bip0322Hash } from '../connect/bip322Signature';
import { BTC_SEGWIT_PATH_PURPOSE, BTC_TAPROOT_PATH_PURPOSE } from '../constant';
import { Recipient } from '../transactions/btc';
import { InputToSign } from '../transactions/psbt';
Expand Down Expand Up @@ -479,87 +477,3 @@ export async function signIncomingSingleSigPSBT({

return psbt.toBase64();
}

/**
* This function is used to sign an incoming BIP 322 message with the ledger
* @param transport - the transport object with connected ledger device
* @param networkType - the network type (Mainnet or Testnet)
* @param addressIndex - the index of the account address to sign with
* @param message - the incoming message in string format to sign
* @returns the signature in string (base64) format
* */
export async function signSimpleBip322Message({
transport,
networkType,
addressIndex,
message,
}: {
transport: Transport;
networkType: NetworkType;
addressIndex: number;
message: string;
}) {
const app = new AppClient(transport);
const coinType = getCoinType(networkType);

// Get account details from ledger to not rely on state
const masterFingerPrint = await app.getMasterFingerprint();
const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`);
const accountPolicy = new DefaultWalletPolicy(
'tr(@0/**)',
`[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`,
);

const { internalPubkey, taprootScript } = getTaprootAccountDataFromXpub(extendedPublicKey, addressIndex, networkType);

const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex');
const prevoutIndex = 0xffffffff;
const sequence = 0;
const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]);

const txToSpend = new Transaction();
txToSpend.version = 0;
txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig);
txToSpend.addOutput(taprootScript, 0);

// Need to update input derivation path so the ledger can recognize the inputs to sign
const inputDerivation: TapBip32Derivation = {
path: `${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`,
pubkey: internalPubkey,
masterFingerprint: Buffer.from(masterFingerPrint, 'hex'),
leafHashes: [],
};

const psbtToSign = new Psbt();
psbtToSign.setVersion(0);
psbtToSign.addInput({
hash: txToSpend.getHash(),
index: 0,
sequence: 0,
tapBip32Derivation: [inputDerivation],
tapInternalKey: internalPubkey,
witnessUtxo: {
script: taprootScript,
value: 0,
},
});
psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 });

const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null);
for (const signature of signatures) {
psbtToSign.updateInput(signature[0], {
tapKeySig: signature[1].signature,
});
}

psbtToSign.finalizeAllInputs();
const txToSign = psbtToSign.extractTransaction();

const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]);

const len = encode(txToSign.ins[0].witness.length);
const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]);

const signature = result.toString('base64');
return signature;
}
167 changes: 167 additions & 0 deletions ledger/btcMessageSigning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import AppClient, { DefaultWalletPolicy } from 'ledger-bitcoin';
import { AddressType, getAddressInfo } from 'bitcoin-address-validation';
import { Psbt, Transaction } from 'bitcoinjs-lib';
import { encode } from 'varuint-bitcoin';
import { BTC_SEGWIT_PATH_PURPOSE, BTC_TAPROOT_PATH_PURPOSE } from '../constant';
import { bip0322Hash } from '../connect';
import { NetworkType } from '../types';
import { getCoinType, getNativeSegwitAccountDataFromXpub, getTaprootAccountDataFromXpub } from './helper';
import { Bip32Derivation, TapBip32Derivation, Transport } from './types';

const encodeVarString = (b: Buffer) => Buffer.concat([encode(b.byteLength), b]);
const DUMMY_INPUT_HASH = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex');
const DUMMY_INPUT_INDEX = 0xffffffff;
const DUMMY_INPUT_SEQUENCE = 0;
type PsbtInput = Parameters<Psbt['addInput']>[0];

const createMessageSignature = async (
app: AppClient,
accountPolicy: DefaultWalletPolicy,
message: string,
witnessScript: Buffer,
inputArgs: Pick<PsbtInput, 'bip32Derivation'> | Pick<PsbtInput, 'tapBip32Derivation' | 'tapInternalKey'>,
isSegwit: boolean,
): Promise<string> => {
const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]);
const txToSpend = new Transaction();
txToSpend.version = 0;
txToSpend.addInput(DUMMY_INPUT_HASH, DUMMY_INPUT_INDEX, DUMMY_INPUT_SEQUENCE, scriptSig);
txToSpend.addOutput(witnessScript, 0);
const psbtToSign = new Psbt();
psbtToSign.setVersion(0);
psbtToSign.addInput({
hash: txToSpend.getHash(),
index: 0,
sequence: 0,
witnessUtxo: {
script: witnessScript,
value: 0,
},
...inputArgs,
});
psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 });
const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null);
for (const signature of signatures) {
if (isSegwit) {
psbtToSign.updateInput(signature[0], {
partialSig: [signature[1]],
});
} else {
psbtToSign.updateInput(signature[0], {
tapKeySig: signature[1].signature,
});
}
}
psbtToSign.finalizeAllInputs();
const txToSign = psbtToSign.extractTransaction();
const len = encode(txToSign.ins[0].witness.length);
const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]);
const signature = result.toString('base64');
return signature;
};

const createSegwitBip322Signature = async ({
message,
app,
addressIndex,
networkType,
}: {
message: string;
app: AppClient;
addressIndex: number;
networkType: NetworkType;
}): Promise<string> => {
const coinType = getCoinType(networkType);
const masterFingerPrint = await app.getMasterFingerprint();
const extendedPublicKey = await app.getExtendedPubkey(`${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'`);
const { publicKey, witnessScript } = getNativeSegwitAccountDataFromXpub(extendedPublicKey, addressIndex, networkType);
const inputDerivation: Bip32Derivation = {
path: `${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`,
pubkey: publicKey,
masterFingerprint: Buffer.from(masterFingerPrint, 'hex'),
};
const accountPolicy = new DefaultWalletPolicy(
'wpkh(@0/**)',
`[${masterFingerPrint}/84'/${coinType}'/0']${extendedPublicKey}`,
);
return createMessageSignature(
app,
accountPolicy,
message,
witnessScript,
{
bip32Derivation: [inputDerivation],
},
true,
);
};

const createTaprootBip322Signature = async ({
message,
app,
addressIndex,
networkType,
}: {
message: string;
app: AppClient;
addressIndex: number;
networkType: NetworkType;
}): Promise<string> => {
const coinType = getCoinType(networkType);
const masterFingerPrint = await app.getMasterFingerprint();
const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`);
const { internalPubkey, taprootScript } = getTaprootAccountDataFromXpub(extendedPublicKey, addressIndex, networkType);
// Need to update input derivation path so the ledger can recognize the inputs to sign
const inputDerivation: TapBip32Derivation = {
path: `${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`,
pubkey: internalPubkey,
masterFingerprint: Buffer.from(masterFingerPrint, 'hex'),
leafHashes: [],
};
const accountPolicy = new DefaultWalletPolicy(
'tr(@0/**)',
`[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`,
);
return createMessageSignature(
app,
accountPolicy,
message,
taprootScript,
{
tapBip32Derivation: [inputDerivation],
tapInternalKey: internalPubkey,
},
false,
);
};

/**
* This function is used to sign an incoming BIP 322 message with the ledger
* @param transport - the transport object with connected ledger device
* @param networkType - the network type (Mainnet or Testnet)
* @param addressIndex - the index of the account address to sign with
* @param message - the incoming message in string format to sign
* @returns the signature in string (base64) format
* */
export async function signSimpleBip322Message({
transport,
networkType,
addressIndex,
address,
message,
}: {
transport: Transport;
networkType: NetworkType;
addressIndex: number;
address: string;
message: string;
}) {
const app = new AppClient(transport);
const { type } = getAddressInfo(address);
if (type === AddressType.p2tr) {
return createTaprootBip322Signature({ message, app, addressIndex, networkType });
} else if (type === AddressType.p2wpkh) {
return createSegwitBip322Signature({ message, app, addressIndex, networkType });
}
throw new Error('Invalid Address Type');
}
1 change: 1 addition & 0 deletions ledger/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './btc';
export * from './psbt';
export * from './stx';
export * from './btcMessageSigning';
export * from './types';

export { getMasterFingerPrint } from './helper';
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@secretkeylabs/xverse-core",
"version": "9.1.2",
"version": "10.0.0",
"description": "",
"engines": {
"node": "^18.18.2"
Expand Down
Loading

0 comments on commit fb5e1cf

Please sign in to comment.