Skip to content

Commit 30b9b85

Browse files
Merge branch 'master' into rel/rc
2 parents e2972fb + da54445 commit 30b9b85

File tree

6 files changed

+254
-122
lines changed

6 files changed

+254
-122
lines changed

modules/sdk-coin-avaxp/src/avaxp.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,21 @@ export class AvaxP extends BaseCoin {
154154
* Signs Avaxp transaction
155155
*/
156156
async signTransaction(params: AvaxpSignTransactionOptions): Promise<SignedTransaction> {
157+
// deserialize raw transaction (note: fromAddress has onchain order)
157158
const txBuilder = this.getBuilder().from(params.txPrebuild.txHex);
158159
const key = params.prv;
160+
161+
// push the keypair to signer array
159162
txBuilder.sign({ key });
160163

164+
// build the transaction
161165
const transaction: BaseTransaction = await txBuilder.build();
162166
if (!transaction) {
163167
throw new InvalidTransactionError('Error while trying to build transaction');
164168
}
165-
const response = {
166-
txHex: transaction.toBroadcastFormat(),
167-
};
168-
169-
return transaction.signature.length >= 2 ? response : { halfSigned: response };
169+
return transaction.signature.length >= 2
170+
? { txHex: transaction.toBroadcastFormat() }
171+
: { halfSigned: { txHex: transaction.toBroadcastFormat() } };
170172
}
171173

172174
async feeEstimate(params: FeeEstimateOptions): Promise<TransactionFee> {

modules/sdk-coin-avaxp/src/lib/delegatorTxBuilder.ts

Lines changed: 126 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -229,19 +229,20 @@ export class DelegatorTxBuilder extends TransactionBuilder {
229229
* @param inputs
230230
* @protected
231231
*/
232-
protected recoverUtxos(inputs: TransferableInput[]): DecodedUtxoObj[] {
233-
return inputs.map((input) => {
234-
const secpInput: SECPTransferInput = input.getInput() as SECPTransferInput;
235-
// Order Addresses as output was defined.
232+
protected recoverUtxos(utxos: TransferableInput[]): DecodedUtxoObj[] {
233+
return utxos.map((utxo) => {
234+
const secpInput: SECPTransferInput = utxo.getInput() as SECPTransferInput;
235+
236+
// use the same addressesIndex as existing ones in the inputs
236237
const addressesIndex: number[] = secpInput.getSigIdxs().map((s) => s.toBuffer().readUInt32BE(0));
237238

238239
return {
239240
outputID: 7,
240-
outputidx: utils.cb58Encode(input.getOutputIdx()),
241-
txid: utils.cb58Encode(input.getTxID()),
241+
outputidx: utils.cb58Encode(utxo.getOutputIdx()),
242+
txid: utils.cb58Encode(utxo.getTxID()),
242243
amount: secpInput.getAmount().toString(),
243244
threshold: this.transaction._threshold,
244-
addresses: [],
245+
addresses: [], // this is empty since the inputs from deserialized transaction don't contain addresses
245246
addressesIndex,
246247
};
247248
});
@@ -250,7 +251,7 @@ export class DelegatorTxBuilder extends TransactionBuilder {
250251
/**
251252
* Threshold must be 2 and since output always get reordered we want to make sure we can always add signatures in the correct location
252253
* To find the correct location for the signature, we use the ouput's addresses to create the signatureIdx in the order that we desire
253-
* 0: user key, 1: recovery key, 2: hsm key
254+
* 0: user key, 1: hsm key, 2: recovery key
254255
* @protected
255256
*/
256257
protected createInputOutput(): {
@@ -260,90 +261,132 @@ export class DelegatorTxBuilder extends TransactionBuilder {
260261
} {
261262
const inputs: TransferableInput[] = [];
262263
const outputs: TransferableOutput[] = [];
263-
const addresses = this.transaction._fromAddresses.map((b) =>
264+
265+
// amount spent so far
266+
let currentTotal: BN = new BN(0);
267+
268+
// delegating and validating have no fees
269+
const totalTarget = this._stakeAmount.clone();
270+
271+
const credentials: Credential[] = [];
272+
273+
// convert fromAddresses to string
274+
// fromAddresses = bitgo order if we are in WP
275+
// fromAddresses = onchain order if we are in from
276+
const bitgoAddresses = this.transaction._fromAddresses.map((b) =>
264277
utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, b)
265278
);
266-
let total: BN = new BN(0);
267-
const totalTarget = this._stakeAmount.clone().add(this.transaction._txFee);
268-
const credentials: Credential[] = [];
269279

270-
this.transaction._utxos.forEach((outputs) => {
271-
if (!outputs.addressesIndex || outputs.addressesIndex.length == 0) {
272-
outputs.addressesIndex = addresses.map((a) => outputs.addresses.indexOf(a));
280+
/*
281+
A = user key
282+
B = hsm key
283+
C = backup key
284+
bitgoAddresses = bitgo addresses [ A, B, C ]
285+
utxo.addresses = IMS addresses [ B, C, A ]
286+
utxo.addressesIndex = [ 2, 0, 1 ]
287+
we pick 0, 1 for non-recovery
288+
we pick 1, 2 for recovery
289+
*/
290+
this.transaction._utxos.forEach((utxo) => {
291+
// in WP, output.addressesIndex is empty, so fill it
292+
if (!utxo.addressesIndex || utxo.addressesIndex.length === 0) {
293+
utxo.addressesIndex = bitgoAddresses.map((a) => utxo.addresses.indexOf(a));
273294
}
295+
// in OVC, output.addressesIndex is defined correctly from the previous iteration
274296
});
275297

276-
this.transaction._utxos
277-
.filter(
278-
(output) =>
279-
output.threshold === this.transaction._threshold &&
280-
(output.addressesIndex?.length == 3 || !output.addressesIndex?.includes(-1))
281-
)
282-
.forEach((output, i) => {
283-
if (output.outputID === 7 && total.lte(totalTarget)) {
284-
const txidBuf = utils.cb58Decode(output.txid);
285-
const amt: BN = new BN(output.amount);
286-
const outputidx = utils.cb58Decode(output.outputidx);
287-
const addressesIndex = output.addressesIndex ?? [];
288-
const isRawUtxos = output.addresses.length == 0;
289-
const firstIndex = this.recoverSigner ? 1 : 0;
290-
total = total.add(amt);
291-
292-
const secpTransferInput = new SECPTransferInput(amt);
293-
294-
if (isRawUtxos) {
295-
addressesIndex.forEach((i) => secpTransferInput.addSignatureIdx(i, this.transaction._fromAddresses[i]));
298+
// validate the utxos
299+
this.transaction._utxos.forEach((utxo) => {
300+
if (!utxo) {
301+
throw new BuildTransactionError('Utxo is undefined');
302+
}
303+
// addressesIndex should neve have a mismatch
304+
if (utxo.addressesIndex?.includes(-1)) {
305+
throw new BuildTransactionError('Addresses are inconsistent');
306+
}
307+
if (utxo.threshold !== this.transaction._threshold) {
308+
throw new BuildTransactionError('Threshold is inconsistent');
309+
}
310+
});
311+
312+
// if we are in OVC, none of the utxos will have addresses since they come from
313+
// deserialized inputs (which don't have addresses), not the IMS
314+
const buildOutputs = this.transaction._utxos[0].addresses.length !== 0;
315+
316+
this.transaction._utxos.forEach((utxo, i) => {
317+
if (utxo.outputID === 7 && currentTotal.lte(totalTarget)) {
318+
const txidBuf = utils.cb58Decode(utxo.txid);
319+
const amt: BN = new BN(utxo.amount);
320+
const outputidx = utils.cb58Decode(utxo.outputidx);
321+
const addressesIndex = utxo.addressesIndex ?? [];
322+
323+
// either user (0) or recovery (2)
324+
const firstIndex = this.recoverSigner ? 2 : 0; // 0
325+
const bitgoIndex = 1;
326+
currentTotal = currentTotal.add(amt);
327+
328+
const secpTransferInput = new SECPTransferInput(amt);
329+
330+
if (!buildOutputs) {
331+
addressesIndex.forEach((i) => secpTransferInput.addSignatureIdx(i, this.transaction._fromAddresses[i]));
332+
} else {
333+
// if user/backup > bitgo
334+
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
335+
// console.log('bitgo < user', addressesIndex[bitgoIndex], addressesIndex[firstIndex]);
336+
secpTransferInput.addSignatureIdx(addressesIndex[bitgoIndex], this.transaction._fromAddresses[bitgoIndex]);
337+
secpTransferInput.addSignatureIdx(addressesIndex[firstIndex], this.transaction._fromAddresses[firstIndex]);
338+
credentials.push(
339+
SelectCredentialClass(
340+
secpTransferInput.getCredentialID(), // 9
341+
['', this.transaction._fromAddresses[firstIndex].toString('hex')].map(utils.createSig)
342+
)
343+
);
296344
} else {
297-
if (addressesIndex[firstIndex] > addressesIndex[2]) {
298-
secpTransferInput.addSignatureIdx(addressesIndex[2], this.transaction._fromAddresses[2]);
299-
secpTransferInput.addSignatureIdx(
300-
addressesIndex[firstIndex],
301-
this.transaction._fromAddresses[firstIndex]
302-
);
303-
credentials.push(
304-
SelectCredentialClass(
305-
secpTransferInput.getCredentialID(),
306-
['', this.transaction._fromAddresses[firstIndex].toString('hex')].map(utils.createSig)
307-
)
308-
);
309-
} else {
310-
secpTransferInput.addSignatureIdx(
311-
addressesIndex[firstIndex],
312-
this.transaction._fromAddresses[firstIndex]
313-
);
314-
secpTransferInput.addSignatureIdx(addressesIndex[2], this.transaction._fromAddresses[2]);
315-
credentials.push(
316-
SelectCredentialClass(
317-
secpTransferInput.getCredentialID(),
318-
[this.transaction._fromAddresses[firstIndex].toString('hex'), ''].map(utils.createSig)
319-
)
320-
);
321-
}
345+
// console.log('user < bitgo', addressesIndex[firstIndex], addressesIndex[bitgoIndex]);
346+
secpTransferInput.addSignatureIdx(addressesIndex[firstIndex], this.transaction._fromAddresses[firstIndex]);
347+
secpTransferInput.addSignatureIdx(addressesIndex[bitgoIndex], this.transaction._fromAddresses[bitgoIndex]);
348+
credentials.push(
349+
SelectCredentialClass(
350+
secpTransferInput.getCredentialID(),
351+
[this.transaction._fromAddresses[firstIndex].toString('hex'), ''].map(utils.createSig)
352+
)
353+
);
322354
}
323-
324-
const input: TransferableInput = new TransferableInput(
325-
txidBuf,
326-
outputidx,
327-
this.transaction._assetId,
328-
secpTransferInput
329-
);
330-
inputs.push(input);
331355
}
332-
});
333-
if (total.lt(totalTarget)) {
334-
throw new BuildTransactionError(`Utxo outputs get ${total.toString()} and ${totalTarget.toString()} is required`);
335-
}
336-
outputs.push(
337-
new TransferableOutput(
338-
this.transaction._assetId,
339-
new SECPTransferOutput(
340-
total.sub(totalTarget),
341-
this.transaction._fromAddresses,
342-
this.transaction._locktime,
343-
this.transaction._threshold
356+
357+
const input: TransferableInput = new TransferableInput(
358+
txidBuf,
359+
outputidx,
360+
this.transaction._assetId,
361+
secpTransferInput
362+
);
363+
inputs.push(input);
364+
}
365+
});
366+
367+
if (buildOutputs) {
368+
if (currentTotal.lt(totalTarget)) {
369+
throw new BuildTransactionError(
370+
`Utxo outputs get ${currentTotal.toString()} and ${totalTarget.toString()} is required`
371+
);
372+
}
373+
outputs.push(
374+
new TransferableOutput(
375+
this.transaction._assetId,
376+
new SECPTransferOutput(
377+
currentTotal.sub(totalTarget),
378+
this.transaction._fromAddresses,
379+
this.transaction._locktime,
380+
this.transaction._threshold
381+
)
344382
)
345-
)
346-
);
347-
return { inputs, outputs, credentials: credentials.length == 0 ? this.transaction.credentials : credentials };
383+
);
384+
}
385+
// get outputs and credentials from the deserialized transaction if we are in OVC
386+
return {
387+
inputs,
388+
outputs: outputs.length === 0 ? this.transaction.avaxPTransaction.getOuts() : outputs,
389+
credentials: credentials.length === 0 ? this.transaction.credentials : credentials,
390+
};
348391
}
349392
}

modules/sdk-coin-avaxp/src/lib/transaction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ function generateSelectorSignature(signatures: signatureSerialized[]): CheckSign
3737
// Look for address.
3838
return function (sig, address): boolean {
3939
try {
40-
if (!isEmptySignature(sig.bytes)) return false;
40+
if (!isEmptySignature(sig.bytes)) {
41+
return false;
42+
}
4143
const pub = sig.bytes.substring(90);
4244
return pub === address;
4345
} catch (e) {

modules/sdk-coin-avaxp/src/lib/transactionBuilder.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,15 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
9090
throw new Error('Network or blockchain is not equals');
9191
}
9292
this._transaction._memo = baseTx.getMemo();
93-
const out = baseTx.getOuts()[0];
94-
if (!out.getAssetID().equals(this._transaction._assetId)) {
93+
94+
// good assumption: addresses that unlock the outputs, will also be used to sign the transaction
95+
// so pick the first utxo as the from address
96+
const utxo = baseTx.getOuts()[0];
97+
98+
if (!utxo.getAssetID().equals(this._transaction._assetId)) {
9599
throw new Error('AssetID are not equals');
96100
}
97-
const secpOut = out.getOutput();
101+
const secpOut = utxo.getOutput();
98102
this._transaction._locktime = secpOut.getLocktime();
99103
this._transaction._threshold = secpOut.getThreshold();
100104
this._transaction._fromAddresses = secpOut.getAddresses();

0 commit comments

Comments
 (0)