Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/bitcore-wallet-client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2062,6 +2062,21 @@ export class API extends EventEmitter {
}
}

/**
* Prepare a transaction proposal for signing.
* Assigns JIT values (nonce, and in the future: fee, gas) to a deferred txp.
* Call this just before signing a deferred-nonce txp.
*/
async prepareTx(opts: { txp: Txp }): Promise<Txp> {
$.checkState(this.credentials && this.credentials.isComplete(),
'Failed state: this.credentials at <prepareTx()>');

const url = '/v1/txproposals/' + opts.txp.id + '/prepare/';
const { body: txp } = await this.request.post<object, Txp>(url, {});
this._processTxps(txp);
return txp;
}

/**
* Create advertisement for bitpay app - (limited to marketing staff)
* @returns {object} Returns the created advertisement
Expand Down
5 changes: 3 additions & 2 deletions packages/bitcore-wallet-service/src/lib/chain/eth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,10 @@ export class EthChain implements IChain {
}
const unsignedTxs = [];

const nonceNum = txp.nonce != null ? Number(txp.nonce) : 0;
if (multiSendContractAddress) {
const multiSendParams = {
nonce: Number(txp.nonce),
nonce: nonceNum,
recipients,
contractAddress: multiSendContractAddress
};
Expand All @@ -330,7 +331,7 @@ export class EthChain implements IChain {
// Uses gas limit from the txp output level
const params = {
...recipients[index],
nonce: Number(txp.nonce) + Number(index),
nonce: nonceNum + Number(index),
recipients: [recipients[index]]
};
unsignedTxs.push(Transactions.create({ ...txp, chain, ...params }));
Expand Down
13 changes: 13 additions & 0 deletions packages/bitcore-wallet-service/src/lib/expressapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,19 @@ export class ExpressApp {
});
*/

router.post('/v1/txproposals/:id/prepare/', (req, res) => {
getServerWithAuth(req, res, server => {
req.body.txProposalId = req.params['id'];
server.prepareTx(req.body, (err, txp) => {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});



//
router.post('/v1/txproposals/:id/publish/', (req, res) => {
getServerWithAuth(req, res, server => {
Expand Down
8 changes: 8 additions & 0 deletions packages/bitcore-wallet-service/src/lib/model/txproposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface ITxProposal {
signingMethod: string;
lowFees?: boolean;
nonce?: number | string;
deferNonce?: boolean;
gasPrice?: number;
maxGasFee?: number;
priorityGasFee?: number;
Expand Down Expand Up @@ -150,6 +151,7 @@ export class TxProposal implements ITxProposal {
lowFees?: boolean;
raw?: Array<string> | string;
nonce?: number | string;
deferNonce?: boolean;
gasPrice?: number;
maxGasFee?: number;
priorityGasFee?: number;
Expand Down Expand Up @@ -273,6 +275,7 @@ export class TxProposal implements ITxProposal {
x.txType = opts.txType;
x.from = opts.from;
x.nonce = opts.nonce;
x.deferNonce = opts.deferNonce;
x.gasLimit = opts.gasLimit; // Backward compatibility for BWC <= 8.9.0
x.data = opts.data; // Backward compatibility for BWC <= 8.9.0
x.tokenAddress = opts.tokenAddress;
Expand Down Expand Up @@ -363,6 +366,7 @@ export class TxProposal implements ITxProposal {
x.txType = obj.txType;
x.from = obj.from;
x.nonce = obj.nonce;
x.deferNonce = obj.deferNonce;
x.gasLimit = obj.gasLimit; // Backward compatibility for BWC <= 8.9.0
x.data = obj.data; // Backward compatibility for BWC <= 8.9.0
x.tokenAddress = obj.tokenAddress;
Expand Down Expand Up @@ -531,6 +535,10 @@ export class TxProposal implements ITxProposal {
return !!this.refreshOnPublish;
}

hasMutableTxData() {
return this.isRepublishEnabled() || !!this.deferNonce;
}

isTemporary() {
return this.status === 'temporary';
}
Expand Down
75 changes: 69 additions & 6 deletions packages/bitcore-wallet-service/src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2694,7 +2694,7 @@ export class WalletService implements IWalletService {
},
async next => {
// SOL is skipped since its a non necessary field that is expected to be provided by the client.
if (!opts.nonce && !Constants.SVM_CHAINS[wallet.chain.toUpperCase()]) {
if (!opts.nonce && !Constants.SVM_CHAINS[wallet.chain.toUpperCase()] && !opts.deferNonce) {
try {
opts.nonce = await ChainService.getTransactionCount(this, wallet, opts.from);
} catch (error) {
Expand Down Expand Up @@ -2794,7 +2794,8 @@ export class WalletService implements IWalletService {
memo: opts.memo,
fromAta: opts.fromAta,
decimals: opts.decimals,
refreshOnPublish: opts.refreshOnPublish
refreshOnPublish: opts.refreshOnPublish,
deferNonce: opts.deferNonce
};
txp = TxProposal.create(txOpts);
next();
Expand Down Expand Up @@ -2906,7 +2907,7 @@ export class WalletService implements IWalletService {
this.storage.fetchTx(this.walletId, opts.txProposalId, (err, txp) => {
if (err) return cb(err);
if (!txp) return cb(Errors.TX_NOT_FOUND);
if (!txp.isTemporary() && !txp.isRepublishEnabled()) return cb(null, txp);
if (!txp.isTemporary() && !txp.hasMutableTxData()) return cb(null, txp);

const copayer = wallet.getCopayer(this.copayerId);

Expand All @@ -2920,7 +2921,7 @@ export class WalletService implements IWalletService {
let signingKey = this._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
if (!signingKey) {
// If the txp has been published previously, we will verify the signature against the previously published raw tx
if (txp.isRepublishEnabled() && txp.prePublishRaw) {
if (txp.hasMutableTxData() && txp.prePublishRaw) {
raw = txp.prePublishRaw;
signingKey = this._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
}
Expand All @@ -2943,7 +2944,7 @@ export class WalletService implements IWalletService {
txp.status = 'pending';
ChainService.refreshTxData(this, txp, opts, (err, txp) => {
if (err) return cb(err);
if (txp.isRepublishEnabled() && !txp.prePublishRaw) {
if (txp.hasMutableTxData() && !txp.prePublishRaw) {
// We save the original raw transaction for verification on republish
txp.prePublishRaw = raw;
}
Expand Down Expand Up @@ -3207,7 +3208,7 @@ export class WalletService implements IWalletService {
try {
const txps = await this.getPendingTxsPromise({});
for (const t of txps) {
if (t.id !== txp.id && t.nonce <= txp.nonce && t.status !== 'rejected') {
if (t.id !== txp.id && t.nonce != null && txp.nonce != null && t.nonce <= txp.nonce && t.status !== 'rejected') {
return cb(Errors.TX_NONCE_CONFLICT);
}
}
Expand Down Expand Up @@ -3268,6 +3269,68 @@ export class WalletService implements IWalletService {
});
}

/**
* Prepare a transaction proposal for signing.
* Assigns JIT values (nonce, and in the future: fee, gas) to a deferred txp.
* Called by the client just before signing.
* @param {Object} opts
* @param {string} opts.txProposalId - The identifier of the transaction.
*/
prepareTx(opts, cb) {
if (!checkRequired(opts, ['txProposalId'], cb)) return;

this._runLocked(cb, cb => {
this.getWallet({}, (err, wallet) => {
if (err) return cb(err);

this.storage.fetchTx(this.walletId, opts.txProposalId, async (err, txp) => {
if (err) return cb(err);
if (!txp) return cb(Errors.TX_NOT_FOUND);
if (!txp.isPending()) return cb(Errors.TX_NOT_PENDING);

if (!txp.deferNonce) {
// Not a deferred-nonce txp. Return it as-is
return cb(null, txp);
}

if (!Constants.EVM_CHAINS[wallet.chain.toUpperCase()]) {
return cb(null, txp);
}

try {
// 1. Get confirmed nonce from blockchain
const confirmedNonce = await ChainService.getTransactionCount(this, wallet, txp.from);

// 2. Get pending TXP nonces from BWS's own database
const pendingTxps = await this.getPendingTxsPromise({});
const pendingNonces = pendingTxps
.filter(t => t.id !== txp.id && t.nonce != null && t.status !== 'rejected')
.map(t => Number(t.nonce));

// 3. Calculate gap-free nonce
let suggestedNonce = Number(confirmedNonce);
const allNonces = pendingNonces.sort((a, b) => a - b);
for (const n of allNonces) {
if (n === suggestedNonce) {
suggestedNonce++;
}
}

txp.nonce = suggestedNonce;

// 4. Store the updated txp
this.storage.storeTx(this.walletId, txp, err => {
if (err) return cb(err);
return cb(null, txp);
});
} catch (err) {
return cb(err);
}
});
});
});
}

_processBroadcast(txp, opts, cb) {
$.checkState(txp.txid, 'Failed state: txp.txid undefined at <_processBroadcast()>');
opts = opts || {};
Expand Down
Loading