-
Notifications
You must be signed in to change notification settings - Fork 142
SAC transfer transactions without RPC #861
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
Changes from all commits
8edbe98
3f1fb06
bf8d266
df3228b
6e8ea37
ec12b58
c4fe566
f0dd43b
f599529
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||||||
| import { UnsignedHyper } from '@stellar/js-xdr'; | ||||||||||||||||||
| import { UnsignedHyper, Hyper } from '@stellar/js-xdr'; | ||||||||||||||||||
| import BigNumber from './util/bignumber'; | ||||||||||||||||||
|
|
||||||||||||||||||
| import xdr from './xdr'; | ||||||||||||||||||
|
|
@@ -14,6 +14,12 @@ import { SorobanDataBuilder } from './sorobandata_builder'; | |||||||||||||||||
| import { StrKey } from './strkey'; | ||||||||||||||||||
| import { SignerKey } from './signerkey'; | ||||||||||||||||||
| import { Memo } from './memo'; | ||||||||||||||||||
| // eslint-disable-next-line no-unused-vars | ||||||||||||||||||
| import { Asset } from './asset'; | ||||||||||||||||||
| import { nativeToScVal } from './scval'; | ||||||||||||||||||
| import { Operation } from './operation'; | ||||||||||||||||||
| import { Address } from './address'; | ||||||||||||||||||
| import { Keypair } from './keypair'; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Minimum base fee for transactions. If this fee is below the network | ||||||||||||||||||
|
|
@@ -34,6 +40,14 @@ export const BASE_FEE = '100'; // Stroops | |||||||||||||||||
| */ | ||||||||||||||||||
| export const TimeoutInfinite = 0; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * @typedef {object} SorobanFees | ||||||||||||||||||
| * @property {number} instructions - the number of instructions executed by the transaction | ||||||||||||||||||
| * @property {number} readBytes - the number of bytes read from the ledger by the transaction | ||||||||||||||||||
| * @property {number} writeBytes - the number of bytes written to the ledger by the transaction | ||||||||||||||||||
| * @property {bigint} resourceFee - the fee to be paid for the transaction, in stroops | ||||||||||||||||||
| */ | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * <p>Transaction builder helps constructs a new `{@link Transaction}` using the | ||||||||||||||||||
| * given {@link Account} as the transaction's "source account". The transaction | ||||||||||||||||||
|
|
@@ -576,6 +590,204 @@ export class TransactionBuilder { | |||||||||||||||||
| return this; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Creates and adds an invoke host function operation for transferring SAC tokens. | ||||||||||||||||||
| * This method removes the need for simulation by handling the creation of the | ||||||||||||||||||
| * appropriate authorization entries and ledger footprint for the transfer operation. | ||||||||||||||||||
| * | ||||||||||||||||||
| * @param {string} destination - the address of the recipient of the SAC transfer (should be a valid Stellar address or contract ID) | ||||||||||||||||||
| * @param {Asset} asset - the SAC asset to be transferred | ||||||||||||||||||
| * @param {BigInt} amount - the amount of tokens to be transferred in 7 decimals. IE 1 token with 7 decimals of precision would be represented as "1_0000000" | ||||||||||||||||||
| * @param {SorobanFees} [sorobanFees] - optional Soroban fees for the transaction to override the default fees used | ||||||||||||||||||
| * | ||||||||||||||||||
| * @returns {TransactionBuilder} | ||||||||||||||||||
| */ | ||||||||||||||||||
| addSacTransferOperation(destination, asset, amount, sorobanFees) { | ||||||||||||||||||
| if (BigInt(amount) <= 0n) { | ||||||||||||||||||
| throw new Error('Amount must be a positive integer'); | ||||||||||||||||||
| } else if (BigInt(amount) > Hyper.MAX_VALUE) { | ||||||||||||||||||
Ryang-21 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
| // The largest supported value for SAC is i64 however the contract interface uses i128 which is why we convert it to i128 | ||||||||||||||||||
| throw new Error('Amount exceeds maximum value for i64'); | ||||||||||||||||||
| } | ||||||||||||||||||
Ryang-21 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
|
|
||||||||||||||||||
| if (sorobanFees) { | ||||||||||||||||||
| const { instructions, readBytes, writeBytes, resourceFee } = sorobanFees; | ||||||||||||||||||
| const U32_MAX = 4294967295; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (instructions <= 0 || instructions > U32_MAX) { | ||||||||||||||||||
| throw new Error( | ||||||||||||||||||
| `instructions must be greater than 0 and at most ${U32_MAX}` | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| if (readBytes <= 0 || readBytes > U32_MAX) { | ||||||||||||||||||
| throw new Error( | ||||||||||||||||||
| `readBytes must be greater than 0 and at most ${U32_MAX}` | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| if (writeBytes <= 0 || writeBytes > U32_MAX) { | ||||||||||||||||||
| throw new Error( | ||||||||||||||||||
| `writeBytes must be greater than 0 and at most ${U32_MAX}` | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| if (resourceFee <= 0n || resourceFee > Hyper.MAX_VALUE) { | ||||||||||||||||||
| throw new Error( | ||||||||||||||||||
| 'resourceFee must be greater than 0 and at most i64 max' | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const isDestinationContract = StrKey.isValidContract(destination); | ||||||||||||||||||
| if (!isDestinationContract) { | ||||||||||||||||||
| if ( | ||||||||||||||||||
| !StrKey.isValidEd25519PublicKey(destination) && | ||||||||||||||||||
| !StrKey.isValidMed25519PublicKey(destination) | ||||||||||||||||||
| ) { | ||||||||||||||||||
| throw new Error( | ||||||||||||||||||
| 'Invalid destination address. Must be a valid Stellar address or contract ID.' | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
Ryang-21 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (destination === this.source.accountId()) { | ||||||||||||||||||
| throw new Error('Destination cannot be the same as the source account.'); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const contractId = asset.contractId(this.networkPassphrase); | ||||||||||||||||||
| const functionName = 'transfer'; | ||||||||||||||||||
| const source = this.source.accountId(); | ||||||||||||||||||
| const args = [ | ||||||||||||||||||
| nativeToScVal(source, { type: 'address' }), | ||||||||||||||||||
| nativeToScVal(destination, { type: 'address' }), | ||||||||||||||||||
| nativeToScVal(amount, { type: 'i128' }) | ||||||||||||||||||
| ]; | ||||||||||||||||||
|
Comment on lines
+658
to
+662
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can combine these 😉
Suggested change
|
||||||||||||||||||
| const isAssetNative = asset.isNative(); | ||||||||||||||||||
|
|
||||||||||||||||||
| const auths = new xdr.SorobanAuthorizationEntry({ | ||||||||||||||||||
| credentials: xdr.SorobanCredentials.sorobanCredentialsSourceAccount(), | ||||||||||||||||||
| rootInvocation: new xdr.SorobanAuthorizedInvocation({ | ||||||||||||||||||
| function: | ||||||||||||||||||
| xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( | ||||||||||||||||||
| new xdr.InvokeContractArgs({ | ||||||||||||||||||
| contractAddress: Address.fromString(contractId).toScAddress(), | ||||||||||||||||||
| functionName, | ||||||||||||||||||
| args | ||||||||||||||||||
| }) | ||||||||||||||||||
| ), | ||||||||||||||||||
| subInvocations: [] | ||||||||||||||||||
| }) | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| const footprint = new xdr.LedgerFootprint({ | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can/should use |
||||||||||||||||||
| readOnly: [ | ||||||||||||||||||
| xdr.LedgerKey.contractData( | ||||||||||||||||||
| new xdr.LedgerKeyContractData({ | ||||||||||||||||||
| contract: Address.fromString(contractId).toScAddress(), | ||||||||||||||||||
| key: xdr.ScVal.scvLedgerKeyContractInstance(), | ||||||||||||||||||
| durability: xdr.ContractDataDurability.persistent() | ||||||||||||||||||
| }) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ], | ||||||||||||||||||
| readWrite: [] | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Ledger entries for the destination account | ||||||||||||||||||
| if (isDestinationContract) { | ||||||||||||||||||
| footprint.readWrite().push( | ||||||||||||||||||
| xdr.LedgerKey.contractData( | ||||||||||||||||||
| new xdr.LedgerKeyContractData({ | ||||||||||||||||||
| contract: Address.fromString(contractId).toScAddress(), | ||||||||||||||||||
| key: xdr.ScVal.scvVec([ | ||||||||||||||||||
| nativeToScVal('Balance', { type: 'symbol' }), | ||||||||||||||||||
| nativeToScVal(destination, { type: 'address' }) | ||||||||||||||||||
| ]), | ||||||||||||||||||
| durability: xdr.ContractDataDurability.persistent() | ||||||||||||||||||
| }) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!isAssetNative) { | ||||||||||||||||||
| footprint.readOnly().push( | ||||||||||||||||||
| xdr.LedgerKey.account( | ||||||||||||||||||
| new xdr.LedgerKeyAccount({ | ||||||||||||||||||
| accountId: Keypair.fromPublicKey(asset.getIssuer()).xdrPublicKey() | ||||||||||||||||||
| }) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| } else if (isAssetNative) { | ||||||||||||||||||
| footprint.readWrite().push( | ||||||||||||||||||
| xdr.LedgerKey.account( | ||||||||||||||||||
| new xdr.LedgerKeyAccount({ | ||||||||||||||||||
| accountId: Keypair.fromPublicKey(destination).xdrPublicKey() | ||||||||||||||||||
| }) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else if (asset.getIssuer() !== destination) { | ||||||||||||||||||
| footprint.readWrite().push( | ||||||||||||||||||
| xdr.LedgerKey.trustline( | ||||||||||||||||||
| new xdr.LedgerKeyTrustLine({ | ||||||||||||||||||
| accountId: Keypair.fromPublicKey(destination).xdrPublicKey(), | ||||||||||||||||||
| asset: asset.toTrustLineXDRObject() | ||||||||||||||||||
| }) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Ledger entries for the source account | ||||||||||||||||||
| if (asset.isNative()) { | ||||||||||||||||||
| footprint.readWrite().push( | ||||||||||||||||||
| xdr.LedgerKey.account( | ||||||||||||||||||
| new xdr.LedgerKeyAccount({ | ||||||||||||||||||
| accountId: Keypair.fromPublicKey(source).xdrPublicKey() | ||||||||||||||||||
| }) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else if (asset.getIssuer() !== source) { | ||||||||||||||||||
| footprint.readWrite().push( | ||||||||||||||||||
| xdr.LedgerKey.trustline( | ||||||||||||||||||
| new xdr.LedgerKeyTrustLine({ | ||||||||||||||||||
| accountId: Keypair.fromPublicKey(source).xdrPublicKey(), | ||||||||||||||||||
| asset: asset.toTrustLineXDRObject() | ||||||||||||||||||
| }) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ); | ||||||||||||||||||
Ryang-21 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const defaultPaymentFees = { | ||||||||||||||||||
| instructions: 400_000, | ||||||||||||||||||
| readBytes: 1_000, | ||||||||||||||||||
| writeBytes: 1_000, | ||||||||||||||||||
| resourceFee: BigInt(5_000_000) | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const sorobanData = new xdr.SorobanTransactionData({ | ||||||||||||||||||
| resources: new xdr.SorobanResources({ | ||||||||||||||||||
| footprint, | ||||||||||||||||||
| instructions: sorobanFees | ||||||||||||||||||
| ? sorobanFees.instructions | ||||||||||||||||||
| : defaultPaymentFees.instructions, | ||||||||||||||||||
| diskReadBytes: sorobanFees | ||||||||||||||||||
| ? sorobanFees.readBytes | ||||||||||||||||||
| : defaultPaymentFees.readBytes, | ||||||||||||||||||
| writeBytes: sorobanFees | ||||||||||||||||||
| ? sorobanFees.writeBytes | ||||||||||||||||||
| : defaultPaymentFees.writeBytes | ||||||||||||||||||
|
Comment on lines
+766
to
+774
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it OK to have |
||||||||||||||||||
| }), | ||||||||||||||||||
| ext: new xdr.SorobanTransactionDataExt(0), | ||||||||||||||||||
| resourceFee: new xdr.Int64( | ||||||||||||||||||
| sorobanFees ? sorobanFees.resourceFee : defaultPaymentFees.resourceFee | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||||||||||||||||||
| ) | ||||||||||||||||||
Ryang-21 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
| }); | ||||||||||||||||||
| const operation = Operation.invokeContractFunction({ | ||||||||||||||||||
| contract: contractId, | ||||||||||||||||||
| function: functionName, | ||||||||||||||||||
| args, | ||||||||||||||||||
| auth: [auths] | ||||||||||||||||||
| }); | ||||||||||||||||||
| this.setSorobanData(sorobanData); | ||||||||||||||||||
| return this.addOperation(operation); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * This will build the transaction. | ||||||||||||||||||
| * It will also increment the source account's sequence number by 1. | ||||||||||||||||||
|
|
@@ -662,6 +874,10 @@ export class TransactionBuilder { | |||||||||||||||||
| if (this.sorobanData) { | ||||||||||||||||||
| // @ts-ignore | ||||||||||||||||||
| attrs.ext = new xdr.TransactionExt(1, this.sorobanData); | ||||||||||||||||||
| // Soroban transactions pay the resource fee in addition to the regular fee, so we need to add it here. | ||||||||||||||||||
| attrs.fee = new BigNumber(attrs.fee) | ||||||||||||||||||
| .plus(this.sorobanData.resourceFee()) | ||||||||||||||||||
| .toNumber(); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| // @ts-ignore | ||||||||||||||||||
| attrs.ext = new xdr.TransactionExt(0, xdr.Void); | ||||||||||||||||||
|
|
@@ -723,7 +939,6 @@ export class TransactionBuilder { | |||||||||||||||||
| const innerOps = innerTx.operations.length; | ||||||||||||||||||
|
|
||||||||||||||||||
| const minBaseFee = new BigNumber(BASE_FEE); | ||||||||||||||||||
| let innerInclusionFee = new BigNumber(innerTx.fee).div(innerOps); | ||||||||||||||||||
| let resourceFee = new BigNumber(0); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Do we need to do special Soroban fee handling? We only want the fee-bump | ||||||||||||||||||
|
|
@@ -733,16 +948,15 @@ export class TransactionBuilder { | |||||||||||||||||
| case xdr.EnvelopeType.envelopeTypeTx().value: { | ||||||||||||||||||
| const sorobanData = env.v1().tx().ext().value(); | ||||||||||||||||||
| resourceFee = new BigNumber(sorobanData?.resourceFee() ?? 0); | ||||||||||||||||||
| innerInclusionFee = BigNumber.max( | ||||||||||||||||||
| minBaseFee, | ||||||||||||||||||
| innerInclusionFee.minus(resourceFee) | ||||||||||||||||||
| ); | ||||||||||||||||||
|
|
||||||||||||||||||
| break; | ||||||||||||||||||
| } | ||||||||||||||||||
| default: | ||||||||||||||||||
| break; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const innerInclusionFee = new BigNumber(innerTx.fee) | ||||||||||||||||||
| .minus(resourceFee) | ||||||||||||||||||
| .div(innerOps); | ||||||||||||||||||
| const base = new BigNumber(baseFee); | ||||||||||||||||||
|
|
||||||||||||||||||
| // The fee rate for fee bump is at least the fee rate of the inner transaction | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is ESLint complaining that we only use this import in the doc portion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the ESLint rules does not know that its being used for the jsdoc