Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds helper to predict claimable balance IDs given a transaction. #482

Merged
merged 8 commits into from
Jan 11, 2022
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function encodeMuxedAccountToAddress(account: xdr.MuxedAccount, supportMuxing: b
function encodeMuxedAccount(gAddress: string, id: string): xdr.MuxedAccount;
```

- Adds a helper function `Transaction.getClaimableBalanceId(int)` which lets you pre-determine the hex claimable balance ID of a `createClaimableBalance` operation prior to submission to the network ([#482](https://github.com/stellar/js-stellar-base/pull/482)).

### Breaking Changes

Expand Down
2 changes: 1 addition & 1 deletion src/operations/set_trustline_flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function setTrustLineFlags(opts = {}) {
const attributes = {};

if (typeof opts.flags !== 'object' || Object.keys(opts.flags).length === 0) {
throw new Error('opts.flags must be an map of boolean flags to modify');
throw new Error('opts.flags must be a map of boolean flags to modify');
}

const mapping = {
Expand Down
55 changes: 54 additions & 1 deletion src/transaction.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import map from 'lodash/map';

import xdr from './generated/stellar-xdr_generated';
import { hash } from './hashing';

import { StrKey } from './strkey';
import { Operation } from './operation';
import { Memo } from './memo';
import { TransactionBase } from './transaction_base';
import { encodeMuxedAccountToAddress } from './util/decode_encode_muxed_account';
import {
extractBaseAddress,
encodeMuxedAccountToAddress
} from './util/decode_encode_muxed_account';

/**
* Use {@link TransactionBuilder} to build a transaction object. If you have an
Expand Down Expand Up @@ -213,4 +217,53 @@ export class Transaction extends TransactionBase {

return envelope;
}

/**
* Calculate the claimable balance ID for an operation within the transaction.
*
* @param {integer} opIndex the index of the CreateClaimableBalance op
* @returns {string} a hex string representing the claimable balance ID
*
* @throws {RangeError} for invalid `opIndex` value
* @throws {TypeError} if op at `opIndex` is not `CreateClaimableBalance`
* @throws for general XDR un/marshalling failures
*
* @see https://github.com/stellar/go/blob/d712346e61e288d450b0c08038c158f8848cc3e4/txnbuild/transaction.go#L392-L435
*
*/
getClaimableBalanceId(opIndex) {
// Validate and then extract the operation from the transaction.
if (
!Number.isInteger(opIndex) ||
opIndex < 0 ||
opIndex >= this.operations.length
) {
throw new RangeError('invalid operation index');
}

let op = this.operations[opIndex];
try {
op = Operation.createClaimableBalance(op);
} catch (err) {
throw new TypeError(
`expected createClaimableBalance, got ${op.type}: ${err}`
);
}

// Always use the transaction's *unmuxed* source.
const account = StrKey.decodeEd25519PublicKey(
extractBaseAddress(this.source)
);
const operationId = xdr.OperationId.envelopeTypeOpId(
new xdr.OperationIdId({
sourceAccount: xdr.AccountId.publicKeyTypeEd25519(account),
seqNum: new xdr.SequenceNumber(this.sequence),
opNum: opIndex
})
);

const opIdHash = hash(operationId.toXDR('raw'));
const balanceId = xdr.ClaimableBalanceId.claimableBalanceIdTypeV0(opIdHash);
return balanceId.toXDR('hex');
}
}
71 changes: 71 additions & 0 deletions test/unit/transaction_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,77 @@ describe('Transaction', function() {
);
});
});

describe('knows how to calculate claimable balance IDs', function() {
const address = 'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ';

const makeBuilder = function(source) {
return new StellarBase.TransactionBuilder(source, {
fee: StellarBase.BASE_FEE,
networkPassphrase: StellarBase.Networks.TESTNET,
withMuxing: true
}).setTimeout(StellarBase.TimeoutInfinite);
};

const makeClaimableBalance = function() {
return StellarBase.Operation.createClaimableBalance({
asset: StellarBase.Asset.native(),
amount: '100',
claimants: [
new StellarBase.Claimant(
address,
StellarBase.Claimant.predicateUnconditional()
)
]
});
};

const paymentOp = StellarBase.Operation.payment({
destination: address,
asset: StellarBase.Asset.native(),
amount: '100'
});

it('calculates from transaction src', function() {
let gSource = new StellarBase.Account(address, '1234');

let tx = makeBuilder(gSource)
.addOperation(makeClaimableBalance())
.build();
const balanceId = tx.getClaimableBalanceId(0);
expect(balanceId).to.be.equal(
'00000000536af35c666a28d26775008321655e9eda2039154270484e3f81d72c66d5c26f'
);
});

it('calculates from muxed transaction src as if unmuxed', function() {
let gSource = new StellarBase.Account(address, '1234');
let mSource = new StellarBase.MuxedAccount(gSource, '5678');
let tx = makeBuilder(mSource)
.addOperation(makeClaimableBalance())
.build();

const balanceId = tx.getClaimableBalanceId(0);
expect(balanceId).to.be.equal(
'00000000536af35c666a28d26775008321655e9eda2039154270484e3f81d72c66d5c26f'
);
});

it('throws on invalid operations', function() {
let gSource = new StellarBase.Account(address, '1234');
let tx = makeBuilder(gSource)
.addOperation(paymentOp)
.addOperation(makeClaimableBalance())
.build();

expect(() => tx.getClaimableBalanceId(0)).to.throw(
/createClaimableBalance/
);
expect(() => tx.getClaimableBalanceId(1)).to.not.throw();
expect(() => tx.getClaimableBalanceId(2)).to.throw(/index/);
expect(() => tx.getClaimableBalanceId(-1)).to.throw(/index/);
});
});
});

function expectBuffersToBeEqual(left, right) {
Expand Down
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,8 @@ export class Transaction<
minTime: string;
maxTime: string;
};

getClaimableBalanceId(opIndex: number): string;
}

export const BASE_FEE = '100';
Expand Down