Skip to content

Commit

Permalink
Merge branch 'soroban' into tx-cloner
Browse files Browse the repository at this point in the history
  • Loading branch information
Shaptic committed Jul 28, 2023
2 parents 1d20a53 + 634f9d5 commit 6454049
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 46 deletions.
1 change: 0 additions & 1 deletion config/webpack.config.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const config = {
},
resolve: {
fallback: {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer')
},
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@
"base32.js": "^0.1.0",
"bignumber.js": "^9.1.1",
"buffer": "^6.0.3",
"crc": "^4.3.2",
"crypto-browserify": "^3.12.0",
"js-xdr": "^3.0.0",
"sha.js": "^2.3.6",
"tweetnacl": "^1.0.3"
Expand Down
11 changes: 8 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export {
} from './operation';
export * from './memo';
export { Account } from './account';
export * from './address';
export { Contract } from './contract';
export { MuxedAccount } from './muxed_account';
export { Claimant } from './claimant';
export { Networks } from './network';
Expand All @@ -47,7 +45,14 @@ export {
encodeMuxedAccount
} from './util/decode_encode_muxed_account';

export * from './numbers/index';
//
// Soroban
//

export { Contract } from './contract';
export { Address } from './address';
export * from './numbers';
export * from './scval';
export * from './sorobandata_builder';

export default module.exports;
11 changes: 7 additions & 4 deletions src/operations/restore_footprint.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import xdr from '../xdr';

/**
* Builds a footprint restoration operation. It takes no parameters because the
* relevant footprint is derived from the transaction itself (see
* {@link TransactionBuilder}'s `opts.sorobanData` parameter, which is a
* Builds a footprint restoration operation.
*
* It takes no parameters because the relevant footprint is derived from the
* transaction itself (see {@link TransactionBuilder}'s `opts.sorobanData`
* parameter (or {@link TransactionBuilder.setSorobanData} /
* {@link TransactionBuilder.setLedgerKeys}), which is a
* {@link xdr.SorobanTransactionData} instance that contains fee data & resource
* usage as part of {@link xdr.SorobanResources}).
* usage as part of {@link xdr.SorobanTransactionData}).
*
* @function
* @alias Operation.restoreFootprint
Expand Down
161 changes: 161 additions & 0 deletions src/sorobandata_builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import xdr from './xdr';

/**
* Supports building {@link xdr.SorobanTransactionData} structures with various
* items set to specific values.
*
* This is recommended for when you are building
* {@link Operation.bumpFootprintExpiration} /
* {@link Operation.restoreFootprint} operations to avoid (re)building the entire
* data structure from scratch.
*
* @constructor
*
* @param {string | xdr.SorobanTransactionData} [sorobanData] either a
* base64-encoded string that represents an
* {@link xdr.SorobanTransactionData} instance or an XDR instance itself
* (it will be copied); if omitted, it starts with an empty instance
*
* @example
* // You want to use an existing data blob but override specific parts.
* const newData = new SorobanDataBuilder(existing)
* .setReadOnly(someLedgerKeys)
* .setRefundableFee("1000")
* .build();
*
* // You want an instance from scratch
* const newData = new SorobanDataBuilder()
* .setFootprint([someLedgerKey], [])
* .setRefundableFee("1000")
* .build();
*/
export class SorobanDataBuilder {
_data;

constructor(sorobanData) {
let data;

if (typeof sorobanData === 'string' || ArrayBuffer.isView(sorobanData)) {
data = SorobanDataBuilder.fromXDR(sorobanData);
} else if (!sorobanData) {
data = new xdr.SorobanTransactionData({
resources: new xdr.SorobanResources({
footprint: new xdr.LedgerFootprint({ readOnly: [], readWrite: [] }),
instructions: 0,
readBytes: 0,
writeBytes: 0,
extendedMetaDataSizeBytes: 0
}),
ext: new xdr.ExtensionPoint(0),
refundableFee: new xdr.Int64(0)
});
} else {
data = xdr.SorobanTransactionData.fromXDR(sorobanData.toXDR()); // copy
}

this._data = data;
}

/**
* Decodes and builds a {@link xdr.SorobanTransactionData} instance.
* @param {Uint8Array|Buffer|string} data raw input to decode
* @returns {xdr.SorobanTransactionData}
*/
static fromXDR(data) {
return xdr.SorobanTransactionData.fromXDR(
data,
typeof data === 'string' ? 'base64' : 'raw'
);
}

/**
* Sets the "refundable" fee portion of the Soroban data.
* @param {number | bigint | string} fee the refundable fee to set (int64)
* @returns {SorobanDataBuilder}
*/
setRefundableFee(fee) {
this._data.refundableFee(new xdr.Int64(fee));
return this;
}

/**
* Sets up the resource metrics.
*
* You should almost NEVER need this, as its often generated / provided to you
* by transaction simulation/preflight from a Soroban RPC server.
*
* @param {number} cpuInstrs number of CPU instructions
* @param {number} readBytes number of bytes being read
* @param {number} writeBytes number of bytes being written
* @param {number} metadataBytes number of extended metadata bytes
*
* @returns {SorobanDataBuilder}
*/
setResources(cpuInstrs, readBytes, writeBytes, metadataBytes) {
this._data.resources().instructions(cpuInstrs);
this._data.resources().readBytes(readBytes);
this._data.resources().writeBytes(writeBytes);
this._data.resources().extendedMetaDataSizeBytes(metadataBytes);

return this;
}

/**
* Sets the storage access footprint to be a certain set of ledger keys.
*
* You can also set each field explicitly via
* {@link SorobanDataBuilder.setReadOnly} and
* {@link SorobanDataBuilder.setReadWrite}.
*
* Passing `null|undefined` to either parameter will IGNORE the existing
* values. If you want to clear them, pass `[]`, instead.
*
* @param {xdr.LedgerKey[]|null} [readOnly] the set of ledger keys to set in
* the read-only portion of the transaction's `sorobanData`
* @param {xdr.LedgerKey[]|null} [readWrite] the set of ledger keys to set in
* the read-write portion of the transaction's `sorobanData`
*
* @returns {SorobanDataBuilder}
*/
setFootprint(readOnly, readWrite) {
if (readOnly !== null) {
// null means "leave me alone"
this.setReadOnly(readOnly);
}
if (readWrite !== null) {
this.setReadWrite(readWrite);
}
return this;
}

/**
* @param {xdr.LedgerKey[]} readOnly read-only keys in the access footprint
* @returns {SorobanDataBuilder}
*/
setReadOnly(readOnly) {
this._data
.resources()
.footprint()
.readOnly(readOnly ?? []);
return this;
}

/**
* @param {xdr.LedgerKey[]} readWrite read-write keys in the access footprint
* @returns {SorobanDataBuilder}
*/
setReadWrite(readWrite) {
this._data
.resources()
.footprint()
.readWrite(readWrite ?? []);
return this;
}

/**
* @returns {xdr.SorobanTransactionData} a copy of the final data structure
*/
build() {
return xdr.SorobanTransactionData.fromXDR(this._data.toXDR()); // clone
}
}
55 changes: 26 additions & 29 deletions src/transaction_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { FeeBumpTransaction } from './fee_bump_transaction';
import { StrKey } from './strkey';
import { SignerKey } from './signerkey';
import { Memo } from './memo';
import { decodeAddressToMuxedAccount } from './util/decode_encode_muxed_account';
import { SorobanDataBuilder } from './sorobandata_builder';

/**
* Minimum base fee for transactions. If this fee is below the network
Expand Down Expand Up @@ -112,11 +114,15 @@ export const TimeoutInfinite = 0;
* @param {string} [opts.networkPassphrase] passphrase of the
* target Stellar network (e.g. "Public Global Stellar Network ; September
* 2015" for the pubnet)
* @param {xdr.SorobanTransactionData | string} [opts.sorobanData] - an optional xdr instance of SorobanTransactionData
* to be set as .Transaction.Ext.SorobanData. It can be xdr object or base64 string.
* For non-contract(non-Soroban) transactions, this has no effect.
* In the case of Soroban transactions, SorobanTransactionData can be obtained from a prior simulation of
* the transaction with a contract invocation and provides necessary resource estimations.
* @param {xdr.SorobanTransactionData | string} [opts.sorobanData] - an
* optional instance of {@link xdr.SorobanTransactionData} to be set as the
* internal `Transaction.Ext.SorobanData` field (either the xdr object or a
* base64 string). In the case of Soroban transactions, this can be obtained
* from a prior simulation of the transaction with a contract invocation and
* provides necessary resource estimations. You can also use
* {@link SorobanDataBuilder} to construct complicated combinations of
* parameters without mucking with XDR directly. **Note:** For
* non-contract(non-Soroban) transactions, this has no effect.
*
*/
export class TransactionBuilder {
Expand All @@ -141,7 +147,10 @@ export class TransactionBuilder {
this.extraSigners = opts.extraSigners ? [...opts.extraSigners] : null;
this.memo = opts.memo || Memo.none();
this.networkPassphrase = opts.networkPassphrase || null;
this.sorobanData = unmarshalSorobanData(opts.sorobanData);

this.sorobanData = opts.sorobanData
? new SorobanDataBuilder(opts.sorobanData).build()
: null;
}

/**
Expand Down Expand Up @@ -507,20 +516,22 @@ export class TransactionBuilder {
}

/**
* Set the {SorobanTransactionData}. For non-contract(non-Soroban) transactions,
* this setting has no effect.
* In the case of Soroban transactions, set to an instance of
* SorobanTransactionData. This can typically be obtained from the simulation
* response based on a transaction with a InvokeHostFunctionOp.
* It provides necessary resource estimations for contract invocation.
* Set the {SorobanTransactionData}. For non-contract(non-Soroban)
* transactions, this setting has no effect. In the case of Soroban
* transactions, set to an instance of SorobanTransactionData. This can
* typically be obtained from the simulation response based on a transaction
* with a InvokeHostFunctionOp. It provides necessary resource estimations for
* contract invocation.
*
* @param {xdr.SorobanTransactionData | string} sorobanData the SorobanTransactionData as xdr object or base64 string
* to be set as Transaction.Ext.SorobanData.
* @param {xdr.SorobanTransactionData | string} sorobanData the
* {@link xdr.SorobanTransactionData} as a raw xdr object or a base64
* string to be decoded then set as Transaction.Ext.SorobanData
*
* @returns {TransactionBuilder}
* @see {SorobanDataBuilder}
*/
setSorobanData(sorobanData) {
this.sorobanData = unmarshalSorobanData(sorobanData);
this.sorobanData = new SorobanDataBuilder(sorobanData).build();
return this;
}

Expand Down Expand Up @@ -771,17 +782,3 @@ export function isValidDate(d) {
// eslint-disable-next-line no-restricted-globals
return d instanceof Date && !isNaN(d);
}

/**
* local helper function to convert SorobanTransactionData from
* base64 string or xdr object.
* @param {string | xdr.SorobanTransactionData} sorobanData the soroban transaction data
* @returns {xdr.SorobanTransactionData}
*/
function unmarshalSorobanData(sorobanData) {
if (typeof sorobanData === 'string') {
const buffer = Buffer.from(sorobanData, 'base64');
sorobanData = xdr.SorobanTransactionData.fromXDR(buffer);
}
return sorobanData;
}
69 changes: 69 additions & 0 deletions test/unit/sorobandata_builder_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
let xdr = StellarBase.xdr;
let dataBuilder = StellarBase.SorobanDataBuilder;

describe('SorobanTransactionData can be built', function () {
const address = new StellarBase.Address(
StellarBase.Keypair.random().publicKey()
);

const sentinel = new xdr.SorobanTransactionData({
resources: new xdr.SorobanResources({
footprint: new xdr.LedgerFootprint({ readOnly: [], readWrite: [] }),
instructions: 1,
readBytes: 2,
writeBytes: 3,
extendedMetaDataSizeBytes: 4
}),
ext: new xdr.ExtensionPoint(0),
refundableFee: new xdr.Int64(5)
});

const key = xdr.LedgerKey.contractData(
new xdr.LedgerKeyContractData({
contract: address.toScAddress(),
key: address.toScVal(),
durability: xdr.ContractDataDurability.persistent(),
bodyType: xdr.ContractEntryBodyType.dataEntry()
})
);

it('constructs from xdr, base64, and nothing', function () {
new dataBuilder();
const fromRaw = new dataBuilder(sentinel).build();
const fromStr = new dataBuilder(sentinel.toXDR('base64')).build();

expect(fromRaw).to.eql(sentinel);
expect(fromStr).to.eql(sentinel);
});

it('sets properties as expected', function () {
expect(
new dataBuilder().setResources(1, 2, 3, 4).setRefundableFee(5).build()
).to.eql(sentinel);

// this isn't a valid param but we're just checking that setters work
const withFootprint = new dataBuilder().setFootprint([key], [key]).build();
expect(withFootprint.resources().footprint().readOnly()[0]).to.eql(key);
expect(withFootprint.resources().footprint().readWrite()[0]).to.eql(key);
});

it('leaves untouched footprints untouched', function () {
const builder = new dataBuilder();

const data = builder.setFootprint([key], [key]).build();
const data2 = new dataBuilder(data).setFootprint(null, []).build();

expect(data.resources().footprint().readOnly()).to.eql([key]);
expect(data.resources().footprint().readWrite()).to.eql([key]);
expect(data2.resources().footprint().readOnly()).to.eql([key]);
expect(data2.resources().footprint().readWrite()).to.eql([]);
});

it('makes copies on build()', function () {
const builder = new dataBuilder();
const first = builder.build();
const second = builder.setRefundableFee(100).build();

expect(first.refundableFee()).to.not.eql(second.refundableFee());
});
});
Loading

0 comments on commit 6454049

Please sign in to comment.