Skip to content

Commit

Permalink
Add helpers to create CAP23 operations. (#368)
Browse files Browse the repository at this point in the history
* Add Operation.createClaimableBalance helper.

* Add claimClaimableBalance helper.

* Update Changelog.

* Add TS definitions.

* Implement balanceID to match current Horizon implementation.

* Document source attribute.

* Fix return value on cap23 operation helpers.

* Update code based on review.
  • Loading branch information
abuiles authored Sep 24, 2020
1 parent 892986b commit c8695d1
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 7 deletions.
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Unreleased

## Add
- Add the `Claimant` class which helps the creation of claimable balances. ((#367)[https://github.com/stellar/js-stellar-base/pull/367]).
- Add the `Claimant` class which helps the creation of claimable balances. ([#367](https://github.com/stellar/js-stellar-base/pull/367)).
The default behavior of this class it to create claimants with an unconditional predicate if none is passed:

```
Expand Down Expand Up @@ -37,6 +37,38 @@ const claimant = new StellarBase.Claimant(
);
```

- Add `Operation.createClaimableBalance` ([#368](https://github.com/stellar/js-stellar-base/pull/368))
Extend the operation class with a new helper to create claimable balance operations.

```js
const asset = new Asset(
'USD',
'GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7'
);
const amount = '100.0000000';
const claimants = [
new Claimant(
'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ',
Claimant.predicateBeforeAbsoluteTime("4102444800000")
)
];

const op = Operation.createClaimableBalance({
asset,
amount,
claimants
});
```

- Add `Operation.claimClaimableBalance` ([#368](https://github.com/stellar/js-stellar-base/pull/368))
Extend the operation class with a new helper to create claim claimable balance operations. It receives the `balanceId` as exposed by Horizon in the `/claimable_balances` end-point.

```js
const op = Operation.createClaimableBalance({
balanceId: '00000000da0d57da7d4850e7fc10d2a9d0ebc731f7afb40574c03395b17d49149b91f5be',
});
```

### Breaking

- The XDR generated in this code includes breaking changes on the internal XDR library since a bug was fixed which was causing incorrect code to be generated (see https://github.com/stellar/xdrgen/pull/52).
Expand Down
20 changes: 20 additions & 0 deletions src/operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import isNumber from 'lodash/isNumber';
import isFinite from 'lodash/isFinite';
import { best_r } from './util/continued_fraction';
import { Asset } from './asset';
import { Claimant } from './claimant';
import { StrKey } from './strkey';
import xdr from './generated/stellar-xdr_generated';
import * as ops from './operations/index';
Expand Down Expand Up @@ -59,6 +60,8 @@ export const AuthImmutableFlag = 1 << 2;
* * `{@link Operation.inflation}`
* * `{@link Operation.manageData}`
* * `{@link Operation.bumpSequence}`
* * `{@link Operation.createClaimableBalance}`
* * `{@link Operation.claimClaimableBalance}`
*
* @class Operation
*/
Expand Down Expand Up @@ -251,6 +254,21 @@ export class Operation {
result.bumpTo = attrs.bumpTo().toString();
break;
}
case 'createClaimableBalance': {
result.type = 'createClaimableBalance';
result.asset = Asset.fromOperation(attrs.asset());
result.amount = this._fromXDRAmount(attrs.amount());
result.claimants = [];
attrs.claimants().forEach((claimant) => {
result.claimants.push(Claimant.fromXDR(claimant));
});
break;
}
case 'claimClaimableBalance': {
result.type = 'claimClaimableBalance';
result.balanceId = attrs.toXDR('hex');
break;
}
default: {
throw new Error(`Unknown operation: ${operationName}`);
}
Expand Down Expand Up @@ -389,6 +407,8 @@ Operation.allowTrust = ops.allowTrust;
Operation.bumpSequence = ops.bumpSequence;
Operation.changeTrust = ops.changeTrust;
Operation.createAccount = ops.createAccount;
Operation.createClaimableBalance = ops.createClaimableBalance;
Operation.claimClaimableBalance = ops.claimClaimableBalance;
Operation.createPassiveSellOffer = ops.createPassiveSellOffer;
Operation.inflation = ops.inflation;
Operation.manageData = ops.manageData;
Expand Down
33 changes: 33 additions & 0 deletions src/operations/claim_claimable_balance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import xdr from '../generated/stellar-xdr_generated';

/**
* Create a new claim claimable balance operation.
* @function
* @alias Operation.claimClaimableBalance
* @param {object} opts Options object
* @param {string} opts.balanceId - The claimable balance id to be claimed.
* @param {string} [opts.source] - The source account for the operation. Defaults to the transaction's source account.
* @returns {xdr.Operation} Claim claimable balance operation
*
* @example
* const op = Operation.claimClaimableBalance({
* balanceId: '00000000da0d57da7d4850e7fc10d2a9d0ebc731f7afb40574c03395b17d49149b91f5be',
* });
*
*/
export function claimClaimableBalance(opts = {}) {
if (typeof opts.balanceId !== 'string') {
throw new Error('must provide a valid claimable balance Id');
}
const attributes = {};
attributes.balanceId = xdr.ClaimableBalanceId.fromXDR(opts.balanceId, 'hex');
const claimClaimableBalanceOp = new xdr.ClaimClaimableBalanceOp(attributes);

const opAttributes = {};
opAttributes.body = xdr.OperationBody.claimClaimableBalance(
claimClaimableBalanceOp
);
this.setSourceAccount(opAttributes, opts);

return new xdr.Operation(opAttributes);
}
63 changes: 63 additions & 0 deletions src/operations/create_claimable_balance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import xdr from '../generated/stellar-xdr_generated';
import { Asset } from '../asset';

/**
* Create a new claimable balance operation.
* @function
* @alias Operation.createClaimableBalance
* @param {object} opts Options object
* @param {Asset} opts.asset - The asset for the claimable balance.
* @param {string} opts.amount - Amount.
* @param {Claimant[]} opts.claimants - An array of Claimants
* @param {string} [opts.source] - The source account for the operation. Defaults to the transaction's source account.
* @returns {xdr.Operation} Create claimable balance operation
*
* @example
* const asset = new Asset(
* 'USD',
* 'GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7'
* );
* const amount = '100.0000000';
* const claimants = [
* new Claimant(
* 'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ',
* Claimant.predicateBeforeAbsoluteTime("4102444800000")
* )
* ];
*
* const op = Operation.createClaimableBalance({
* asset,
* amount,
* claimants
* });
*
*/
export function createClaimableBalance(opts) {
if (!(opts.asset instanceof Asset)) {
throw new Error(
'must provide an asset for create claimable balance operation'
);
}

if (!this.isValidAmount(opts.amount)) {
throw new TypeError(this.constructAmountRequirementsError('amount'));
}

if (!Array.isArray(opts.claimants) || opts.claimants.length === 0) {
throw new Error('must provide at least one claimant');
}

const attributes = {};
attributes.asset = opts.asset.toXDRObject();
attributes.amount = this._toXDRAmount(opts.amount);
attributes.claimants = opts.claimants.map((c) => c.toXDRObject());
const createClaimableBalanceOp = new xdr.CreateClaimableBalanceOp(attributes);

const opAttributes = {};
opAttributes.body = xdr.OperationBody.createClaimableBalance(
createClaimableBalanceOp
);
this.setSourceAccount(opAttributes, opts);

return new xdr.Operation(opAttributes);
}
2 changes: 2 additions & 0 deletions src/operations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export { allowTrust } from './allow_trust';
export { bumpSequence } from './bump_sequence';
export { changeTrust } from './change_trust';
export { createAccount } from './create_account';
export { createClaimableBalance } from './create_claimable_balance';
export { claimClaimableBalance } from './claim_claimable_balance';
export { inflation } from './inflation';
export { manageData } from './manage_data';
export { manageBuyOffer } from './manage_buy_offer';
Expand Down
118 changes: 118 additions & 0 deletions test/unit/operation_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,124 @@ describe('Operation', function() {
});
});

describe('createClaimableBalance()', function() {
it('creates a CreateClaimableBalanceOp', function() {
const asset = new StellarBase.Asset(
'USD',
'GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7'
);
const amount = '100.0000000';
const claimants = [
new StellarBase.Claimant(
'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ'
)
];

const op = StellarBase.Operation.createClaimableBalance({
asset,
amount,
claimants
});
var xdr = op.toXDR('hex');
var operation = StellarBase.xdr.Operation.fromXDR(
Buffer.from(xdr, 'hex')
);
var obj = StellarBase.Operation.fromXDRObject(operation);
expect(obj.type).to.be.equal('createClaimableBalance');
expect(obj.asset.toString()).to.equal(asset.toString());
expect(obj.amount).to.be.equal(amount);
expect(obj.claimants).to.have.lengthOf(1);
expect(obj.claimants[0].toXDRObject().toXDR('hex')).to.equal(
claimants[0].toXDRObject().toXDR('hex')
);
});
it('throws an error when asset is not present', function() {
const amount = '100.0000000';
const claimants = [
new StellarBase.Claimant(
'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ'
)
];

const attrs = {
amount,
claimants
};

expect(() =>
StellarBase.Operation.createClaimableBalance(attrs)
).to.throw(
/must provide an asset for create claimable balance operation/
);
});
it('throws an error when amount is not present', function() {
const asset = new StellarBase.Asset(
'USD',
'GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7'
);
const claimants = [
new StellarBase.Claimant(
'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ'
)
];

const attrs = {
asset,
claimants
};

expect(() =>
StellarBase.Operation.createClaimableBalance(attrs)
).to.throw(
/amount argument must be of type String, represent a positive number and have at most 7 digits after the decimal/
);
});
it('throws an error when claimants is empty or not present', function() {
const asset = new StellarBase.Asset(
'USD',
'GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7'
);
const amount = '100.0000';

const attrs = {
asset,
amount
};

expect(() =>
StellarBase.Operation.createClaimableBalance(attrs)
).to.throw(/must provide at least one claimant/);

attrs.claimants = [];
expect(() =>
StellarBase.Operation.createClaimableBalance(attrs)
).to.throw(/must provide at least one claimant/);
});
});

describe('claimClaimableBalance()', function() {
it('creates a claimClaimableBalanceOp', function() {
const balanceId =
'00000000da0d57da7d4850e7fc10d2a9d0ebc731f7afb40574c03395b17d49149b91f5be';

const op = StellarBase.Operation.claimClaimableBalance({
balanceId
});
var xdr = op.toXDR('hex');
var operation = StellarBase.xdr.Operation.fromXDR(
Buffer.from(xdr, 'hex')
);
var obj = StellarBase.Operation.fromXDRObject(operation);
expect(obj.type).to.be.equal('claimClaimableBalance');
expect(obj.balanceId).to.equal(balanceId);
});
it('throws an error when balanceId is not present', function() {
expect(() => StellarBase.Operation.claimClaimableBalance({})).to.throw(
/must provide a valid claimable balance Id/
);
});
});

describe('.isValidAmount()', function() {
it('returns true for valid amounts', function() {
let amounts = [
Expand Down
Loading

0 comments on commit c8695d1

Please sign in to comment.