Skip to content

Commit a6fa111

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 1ba2308 + 629fcca commit a6fa111

31 files changed

Lines changed: 770 additions & 162 deletions

‎.github/workflows/e2e-android.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ jobs:
8383
env:
8484
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
8585
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
86+
MNEMONICS_COBO: ${{ secrets.MNEMONICS_COBO }}
8687

8788
steps:
8889
- name: Checkout

‎.github/workflows/e2e-ios.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ jobs:
164164
env:
165165
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
166166
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
167+
MNEMONICS_COBO: ${{ secrets.MNEMONICS_COBO }}
167168

168169
steps:
169170
- name: Checkout

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,4 @@ android/app/.classpath
101101
android/app/.project
102102
fastlane/README.md
103103
fastlane/report.xml
104+
ios/BlueWallet.xcodeproj/xcshareddata/xcodecloud/manifest.json

‎android/app/build.gradle‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ android {
8787
minSdkVersion rootProject.ext.minSdkVersion
8888
targetSdkVersion rootProject.ext.targetSdkVersion
8989
versionCode 1
90-
versionName "8.0.3"
90+
versionName "8.0.4"
9191
testBuildType System.getProperty('testBuildType', 'debug')
9292
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
9393
// Keep compatibility across react-native-capture-protection flavor changes.

‎class/blue-app.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { getLNDHub } from '../helpers/lndHub';
2828
import { LightningArkWallet } from './wallets/lightning-ark-wallet.ts';
2929
import { hexToUint8Array, uint8ArrayToHex } from '../blue_modules/uint8array-extras';
3030
import { HDTaprootWallet } from './wallets/hd-taproot-wallet';
31+
import { TaprootWallet } from './wallets/taproot-wallet';
3132

3233
let usedBucketNum: boolean | number = false;
3334
let savingInProgress = 0; // its both a flag and a counter of attempts to write to disk
@@ -430,6 +431,9 @@ export class BlueApp {
430431
case HDTaprootWallet.type:
431432
unserializedWallet = HDTaprootWallet.fromJson(key) as unknown as HDTaprootWallet;
432433
break;
434+
case TaprootWallet.type:
435+
unserializedWallet = TaprootWallet.fromJson(key) as unknown as TaprootWallet;
436+
break;
433437
case HDLegacyBreadwalletWallet.type:
434438
unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key) as unknown as HDLegacyBreadwalletWallet;
435439
break;

‎class/wallets/legacy-wallet.ts‎

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import BigNumber from 'bignumber.js';
22
import * as bitcoin from 'groestlcoinjs-lib';
33
import bitcoinMessage from 'groestlcoinjs-message';
4-
import coinSelect, { CoinSelectOutput, CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
4+
import coinSelect, { CoinSelectOptions, CoinSelectOutput, CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
55
import coinSelectSplit from 'coinselect/split';
66
import { ECPairAPI, ECPairFactory, Signer } from 'ecpairgrs';
77

@@ -373,6 +373,31 @@ export class LegacyWallet extends AbstractWallet {
373373
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
374374
}
375375

376+
/**
377+
* Whether size of at least one of the targets is overestimated by coinselect lib, comparing to its real scriptPubKey
378+
*/
379+
_targetsAreOverestimated(targets: CreateTransactionTarget[]): boolean {
380+
return targets.some(t => {
381+
if (!t.address) return false;
382+
const address = t.address.toLowerCase(); // bech32 addresses are case insensitive
383+
const assumed = t.script?.length ?? 25; // lib assumes p2pkh
384+
if (address.startsWith('grssp1')) return assumed > 34; // silent payment ends up as p2tr output
385+
try {
386+
return assumed > bitcoin.address.toOutputScript(address.startsWith('grs1') ? address : t.address).length;
387+
} catch (_) {
388+
return false;
389+
}
390+
});
391+
}
392+
393+
/**
394+
* Length of the scriptPubKey of this wallet's change output, in bytes. Passed to coinselect lib when it is bigger than
395+
* p2pkh (25 bytes), which is what the lib assumes
396+
*/
397+
getChangeScriptLength(): number {
398+
return this.segwitType === 'p2tr' ? 34 : 25;
399+
}
400+
376401
coinselect(
377402
utxos: CreateTransactionUtxo[],
378403
targets: CreateTransactionTarget[],
@@ -406,10 +431,11 @@ export class LegacyWallet extends AbstractWallet {
406431

407432
// counting the number of vbytes for each script type:
408433
if (this.segwitType === 'p2wpkh') {
409-
// 72 (high R low S signature) + 1 + 33 (comp pubkey) + 1 = 107 / 4 = 26.75 rounded up.
434+
// witness: 1 (items count) + 1 + 72 (high R low S signature with sighash byte) + 1 + 33 (comp pubkey) = 108 / 4 = 27.
435+
// exact for the biggest signature, no slack here
410436
u.script = { length: 27 };
411437
} else if (this.segwitType === 'p2sh(p2wpkh)') {
412-
// ((72 (high R low S signature) + 1 + 33 (comp pubkey) + 1) / 4) + 22 (P2WPKH output on scriptSig stack) + 1 = 49.75 rounded up
438+
// same 27 vbytes of witness as p2wpkh + 23 bytes of scriptSig (1 + 22 of P2WPKH output on scriptSig stack) = 50
413439
u.script = { length: 50 };
414440
} else if (this.segwitType === 'p2tr') {
415441
// taproot key path spend is just a 64 or 65 byte signature on the witness stack.
@@ -423,18 +449,37 @@ export class LegacyWallet extends AbstractWallet {
423449
}
424450

425451
for (const t of _targets) {
426-
if (t.address?.startsWith('bc1')) {
452+
// bech32 addresses are case insensitive
453+
const address = t.address?.toLowerCase();
454+
if (address?.startsWith('bc1')) {
427455
// in case address is non-typical and takes more bytes than coinselect library anticipates by default
428-
t.script = { length: bitcoin.address.toOutputScript(t.address).length + 3 };
456+
t.script = { length: bitcoin.address.toOutputScript(address).length + 3 };
457+
} else if (address?.startsWith('grssp1')) {
458+
// silent payment address ends up as p2tr output (34 bytes), sizing it the same way as bc1p address above
459+
t.script = { length: 34 + 3 };
429460
}
430461

431462
if (t.script?.hex) {
432463
// setting length for coinselect lib manually as it is not aware of our field `hex`
433-
t.script.length = t.script.hex.length / 2 - 4;
464+
t.script.length = t.script.hex.length / 2;
434465
}
435466
}
436467

437-
const { inputs, outputs, fee } = algo(_utxos, _targets as CoinSelectTarget[], feeRate);
468+
// coinselect lib assumes change output has 25 bytes script (p2pkh). if our change script is bigger (p2tr, p2wsh) we have
469+
// to tell it, otherwise change output is not fully paid for and resulting feerate is lower than requested.
470+
// smaller change scripts are left as is, that only makes us overestimate by 2-3 bytes
471+
const changeScriptLength = this.getChangeScriptLength();
472+
const options: CoinSelectOptions = {};
473+
if (changeScriptLength > 25) options.changeScript = { length: changeScriptLength };
474+
475+
// coinselect lib is not aware of segwit marker & flag (2 bytes = 0.5 vbyte), and since vsize is rounded up the tx can end
476+
// up 1 vbyte bigger than estimated. p2tr inputs are overestimated by 0.5 vbyte each, so only p2wpkh inputs are affected.
477+
// we also dont need it when outputs are already overestimated (`+ 3` above; p2sh is 23 bytes while lib assumes 25)
478+
if ((this.segwitType === 'p2wpkh' || this.segwitType === 'p2sh(p2wpkh)') && !this._targetsAreOverestimated(_targets)) {
479+
options.txExtraBytes = 1;
480+
}
481+
482+
const { inputs, outputs, fee } = algo(_utxos, _targets as CoinSelectTarget[], feeRate, options);
438483

439484
// .inputs and .outputs will be undefined if no solution was found
440485
if (!inputs || !outputs) {
@@ -583,7 +628,7 @@ export class LegacyWallet extends AbstractWallet {
583628
if (!address) return false;
584629
let cleanAddress = address;
585630

586-
if (this.segwitType === 'p2wpkh') {
631+
if (this.segwitType === 'p2wpkh' || this.segwitType === 'p2tr') {
587632
cleanAddress = address.toLowerCase();
588633
}
589634

‎class/wallets/multisig-hd-wallet.ts‎

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
931931
return howManyPrivKeysWeGot;
932932
}
933933

934+
getChangeScriptLength(): number {
935+
// p2wsh is OP_0 + 32 bytes hash
936+
return this.isNativeSegwit() ? 34 : 25;
937+
}
938+
934939
coinselect(
935940
utxos: CreateTransactionUtxo[],
936941
targets: CreateTransactionTarget[],
@@ -953,9 +958,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
953958
length: 35 + Math.ceil((8 + this.getM() * 74 + this.getN() * 34) / 4),
954959
};
955960
} else {
956-
u.script = {
957-
length: 2 + this.getM() * 74 + this.getN() * 34,
958-
};
961+
// scriptSig is: OP_0 + M signatures (push opcode + up to 73 bytes of signature with sighash byte) + pushed redeem script
962+
const redeemScriptLength = 3 + this.getN() * 34;
963+
const pushLength = redeemScriptLength > 255 ? 3 : redeemScriptLength > 75 ? 2 : 1;
964+
let length = 1 + this.getM() * 74 + pushLength + redeemScriptLength;
965+
// coinselect lib counts 1 byte for scriptSig length, but it takes 3 bytes when scriptSig is over 252 bytes
966+
if (length > 252) length += 2;
967+
u.script = { length };
959968
}
960969
}
961970

@@ -1147,15 +1156,50 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
11471156
return goesIn - goesOut;
11481157
}
11491158

1159+
static _isPsbtInputFinalized(input: Psbt['data']['inputs'][number]) {
1160+
return !!(input.finalScriptSig || input.finalScriptWitness);
1161+
}
1162+
1163+
/**
1164+
* Signatures the input has towards the quorum, the same way bitcoinjs counts them when finalizing:
1165+
* only signatures by keys from the input's multisig script
1166+
*/
1167+
_countInputSignatures(psbt: Psbt, inputIndex: number) {
1168+
const input = psbt.data.inputs[inputIndex];
1169+
// finalized input has no partialSig anymore, but it means we have enough
1170+
// He who knows that enough is enough will always have enough. Lao Tzu
1171+
if (MultisigHDWallet._isPsbtInputFinalized(input)) return this.getM();
1172+
1173+
let sigs = 0;
1174+
for (const { pubkey } of input.partialSig || []) {
1175+
try {
1176+
if (psbt.inputHasPubkey(inputIndex, pubkey)) sigs++;
1177+
} catch (_) {
1178+
sigs++; // cannot verify against the script (missing or mismatched), assume the signature counts
1179+
}
1180+
}
1181+
return sigs;
1182+
}
1183+
1184+
/**
1185+
* Finalizes inputs that are not finalized yet, finalizing an input twice throws
1186+
*/
1187+
static finalizeUnfinalizedInputs(psbt: Psbt) {
1188+
for (let cc = 0; cc < psbt.inputCount; cc++) {
1189+
if (!MultisigHDWallet._isPsbtInputFinalized(psbt.data.inputs[cc])) psbt.finalizeInput(cc);
1190+
}
1191+
}
1192+
1193+
/**
1194+
* Number of signatures that every input has, i.e. the least signed input decides
1195+
*/
11501196
calculateHowManySignaturesWeHaveFromPsbt(psbt: Psbt) {
1151-
let sigsHave = 0;
1152-
for (const inp of psbt.data.inputs) {
1153-
sigsHave = Math.max(sigsHave, inp.partialSig?.length || 0);
1154-
if (inp.finalScriptSig || inp.finalScriptWitness) sigsHave = this.getM(); // hacky, but it means we have enough
1155-
// He who knows that enough is enough will always have enough. Lao Tzu
1197+
let sigsHave = Infinity;
1198+
for (let cc = 0; cc < psbt.inputCount; cc++) {
1199+
sigsHave = Math.min(sigsHave, this._countInputSignatures(psbt, cc));
11561200
}
11571201

1158-
return sigsHave;
1202+
return sigsHave === Infinity ? 0 : sigsHave;
11591203
}
11601204

11611205
/**
@@ -1184,7 +1228,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
11841228
}
11851229

11861230
for (let cc = 0; cc < psbt.inputCount; cc++) {
1231+
const input = psbt.data.inputs[cc];
1232+
if (MultisigHDWallet._isPsbtInputFinalized(input)) continue;
1233+
11871234
for (const { cosignerIndex, hdRoot } of hdRoots) {
1235+
// dont sign more than we need, otherwise there will be "Too many signatures" error
1236+
if (this._countInputSignatures(psbt, cc) >= this.getM()) break;
1237+
11881238
try {
11891239
psbt.signInputHD(cc, hdRoot);
11901240
} catch (_) {} // protects agains duplicate cosignings
@@ -1222,8 +1272,8 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
12221272
}
12231273

12241274
if (this.calculateHowManySignaturesWeHaveFromPsbt(psbt) >= this.getM()) {
1225-
const tx = psbt.finalizeAllInputs().extractTransaction();
1226-
return { tx };
1275+
MultisigHDWallet.finalizeUnfinalizedInputs(psbt);
1276+
return { tx: psbt.extractTransaction() };
12271277
}
12281278

12291279
return { tx: false };

‎class/wallets/taproot-wallet.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@ export class TaprootWallet extends SegwitBech32Wallet {
1616
public readonly type = TaprootWallet.type;
1717
// @ts-ignore: override
1818
public readonly typeReadable = TaprootWallet.typeReadable;
19-
public readonly segwitType = 'p2wpkh';
19+
// @ts-ignore: override
20+
public readonly segwitType = 'p2tr';
21+
22+
static fromJson(obj: string): TaprootWallet {
23+
const wallet = super.fromJson(obj) as unknown as TaprootWallet;
24+
// older versions serialized this wallet with segwitType 'p2wpkh', which made coinselect overestimate input size
25+
// this override can be removed in 2028, by then all stored wallets should be re-saved with the correct value
26+
// @ts-ignore: override readonly
27+
wallet.segwitType = 'p2tr';
28+
return wallet;
29+
}
2030

2131
/**
2232
* Converts script pub key to a Taproot address if it can. Returns FALSE if it cant.

‎ios/BlueWallet.xcodeproj/project.pbxproj‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,7 @@
12121212
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
12131213
"$(inherited)",
12141214
);
1215-
MARKETING_VERSION = 8.0.3;
1215+
MARKETING_VERSION = 8.0.4;
12161216
OTHER_LDFLAGS = (
12171217
"$(inherited)",
12181218
"-ObjC",
@@ -1268,7 +1268,7 @@
12681268
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
12691269
"$(inherited)",
12701270
);
1271-
MARKETING_VERSION = 8.0.3;
1271+
MARKETING_VERSION = 8.0.4;
12721272
OTHER_LDFLAGS = (
12731273
"$(inherited)",
12741274
"-ObjC",
@@ -1310,7 +1310,7 @@
13101310
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
13111311
"$(inherited)",
13121312
);
1313-
MARKETING_VERSION = 8.0.3;
1313+
MARKETING_VERSION = 8.0.4;
13141314
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
13151315
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.QuickLookThumbnail;
13161316
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1348,7 +1348,7 @@
13481348
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
13491349
"$(inherited)",
13501350
);
1351-
MARKETING_VERSION = 8.0.3;
1351+
MARKETING_VERSION = 8.0.4;
13521352
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
13531353
MTL_FAST_MATH = YES;
13541354
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@@ -1390,7 +1390,7 @@
13901390
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
13911391
"$(inherited)",
13921392
);
1393-
MARKETING_VERSION = 8.0.3;
1393+
MARKETING_VERSION = 8.0.4;
13941394
MTL_FAST_MATH = YES;
13951395
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
13961396
PRODUCT_BUNDLE_IDENTIFIER = org.groestlcoin.bluewallet123.Stickers;
@@ -1440,7 +1440,7 @@
14401440
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
14411441
"$(inherited)",
14421442
);
1443-
MARKETING_VERSION = 8.0.3;
1443+
MARKETING_VERSION = 8.0.4;
14441444
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
14451445
MTL_FAST_MATH = YES;
14461446
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@@ -1497,7 +1497,7 @@
14971497
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
14981498
"$(inherited)",
14991499
);
1500-
MARKETING_VERSION = 8.0.3;
1500+
MARKETING_VERSION = 8.0.4;
15011501
MTL_FAST_MATH = YES;
15021502
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
15031503
PRODUCT_BUNDLE_IDENTIFIER = org.groestlcoin.bluewallet123.MarketWidget;
@@ -1535,7 +1535,7 @@
15351535
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
15361536
"$(inherited)",
15371537
);
1538-
MARKETING_VERSION = 8.0.3;
1538+
MARKETING_VERSION = 8.0.4;
15391539
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
15401540
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.QuickLookThumbnail;
15411541
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1721,7 +1721,7 @@
17211721
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
17221722
"$(inherited)",
17231723
);
1724-
MARKETING_VERSION = 8.0.3;
1724+
MARKETING_VERSION = 8.0.4;
17251725
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
17261726
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.QuickLookPreview;
17271727
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1764,7 +1764,7 @@
17641764
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
17651765
"$(inherited)",
17661766
);
1767-
MARKETING_VERSION = 8.0.3;
1767+
MARKETING_VERSION = 8.0.4;
17681768
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
17691769
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.QuickLookPreview;
17701770
PRODUCT_NAME = "$(TARGET_NAME)";

0 commit comments

Comments
 (0)