invocation.js

  1. import { Asset } from './asset';
  2. import { Address } from './address';
  3. import { scValToNative } from './scval';
  4. /**
  5. * @typedef CreateInvocation
  6. *
  7. * @prop {'wasm'|'sac'} type a type indicating if this creation was a custom
  8. * contract or a wrapping of an existing Stellar asset
  9. * @prop {string} [token] when `type=='sac'`, the canonical {@link Asset} that
  10. * is being wrapped by this Stellar Asset Contract
  11. * @prop {object} [wasm] when `type=='wasm'`, add'l creation parameters
  12. *
  13. * @prop {string} wasm.hash hex hash of WASM bytecode backing this contract
  14. * @prop {string} wasm.address contract address of this deployment
  15. * @prop {string} wasm.salt hex salt that the user consumed when creating
  16. * this contract (encoded in the resulting address)
  17. * @prop {any[]} [wasm.constructorArgs] a list of natively-represented values
  18. * (see {@link scValToNative}) that are passed to the constructor when
  19. * creating this contract
  20. */
  21. /**
  22. * @typedef ExecuteInvocation
  23. *
  24. * @prop {string} source the strkey of the contract (C...) being invoked
  25. * @prop {string} function the name of the function being invoked
  26. * @prop {any[]} args the natively-represented parameters to the function
  27. * invocation (see {@link scValToNative} for rules on how they're
  28. * represented a JS types)
  29. */
  30. /**
  31. * @typedef InvocationTree
  32. * @prop {'execute' | 'create'} type the type of invocation occurring, either
  33. * contract creation or host function execution
  34. * @prop {CreateInvocation | ExecuteInvocation} args the parameters to the
  35. * invocation, depending on the type
  36. * @prop {InvocationTree[]} invocations any sub-invocations that (may) occur
  37. * as a result of this invocation (i.e. a tree of call stacks)
  38. */
  39. /**
  40. * Turns a raw invocation tree into a human-readable format.
  41. *
  42. * This is designed to make the invocation tree easier to understand in order to
  43. * inform users about the side-effects of their contract calls. This will help
  44. * make informed decisions about whether or not a particular invocation will
  45. * result in what you expect it to.
  46. *
  47. * @param {xdr.SorobanAuthorizedInvocation} root the raw XDR of the invocation,
  48. * likely acquired from transaction simulation. this is either from the
  49. * {@link Operation.invokeHostFunction} itself (the `func` field), or from
  50. * the authorization entries ({@link xdr.SorobanAuthorizationEntry}, the
  51. * `rootInvocation` field)
  52. *
  53. * @returns {InvocationTree} a human-readable version of the invocation tree
  54. *
  55. * @example
  56. * Here, we show a browser modal after simulating an arbitrary transaction,
  57. * `tx`, which we assume has an `Operation.invokeHostFunction` inside of it:
  58. *
  59. * ```typescript
  60. * import { Server, buildInvocationTree } from '@stellar/stellar-sdk';
  61. *
  62. * const s = new Server("fill in accordingly");
  63. *
  64. * s.simulateTransaction(tx).then(
  65. * (resp: SorobanRpc.SimulateTransactionResponse) => {
  66. * if (SorobanRpc.isSuccessfulSim(resp) && ) {
  67. * // bold assumption: there's a valid result with an auth entry
  68. * alert(
  69. * "You are authorizing the following invocation:\n" +
  70. * JSON.stringify(
  71. * buildInvocationTree(resp.result!.auth[0].rootInvocation()),
  72. * null,
  73. * 2
  74. * )
  75. * );
  76. * }
  77. * }
  78. * );
  79. * ```
  80. */
  81. export function buildInvocationTree(root) {
  82. const fn = root.function();
  83. /** @type {InvocationTree} */
  84. const output = {};
  85. /** @type {xdr.CreateContractArgs|xdr.CreateContractArgsV2|xdr.InvokeContractArgs} */
  86. const inner = fn.value();
  87. switch (fn.switch().value) {
  88. // sorobanAuthorizedFunctionTypeContractFn
  89. case 0:
  90. output.type = 'execute';
  91. output.args = {
  92. source: Address.fromScAddress(inner.contractAddress()).toString(),
  93. function: inner.functionName(),
  94. args: inner.args().map((arg) => scValToNative(arg))
  95. };
  96. break;
  97. // sorobanAuthorizedFunctionTypeCreateContractHostFn
  98. // sorobanAuthorizedFunctionTypeCreateContractV2HostFn
  99. case 1: // fallthrough: just no ctor args in V1
  100. case 2: {
  101. const createV2 = fn.switch().value === 2;
  102. output.type = 'create';
  103. output.args = {};
  104. // If the executable is a WASM, the preimage MUST be an address. If it's a
  105. // token, the preimage MUST be an asset. This is a cheeky way to check
  106. // that, because wasm=0, token=1 and address=0, asset=1 in the XDR switch
  107. // values.
  108. //
  109. // The first part may not be true in V2, but we'd need to update this code
  110. // anyway so it can still be an error.
  111. const [exec, preimage] = [inner.executable(), inner.contractIdPreimage()];
  112. if (!!exec.switch().value !== !!preimage.switch().value) {
  113. throw new Error(
  114. `creation function appears invalid: ${JSON.stringify(
  115. inner
  116. )} (should be wasm+address or token+asset)`
  117. );
  118. }
  119. switch (exec.switch().value) {
  120. // contractExecutableWasm
  121. case 0: {
  122. /** @type {xdr.ContractIdPreimageFromAddress} */
  123. const details = preimage.fromAddress();
  124. output.args.type = 'wasm';
  125. output.args.wasm = {
  126. salt: details.salt().toString('hex'),
  127. hash: exec.wasmHash().toString('hex'),
  128. address: Address.fromScAddress(details.address()).toString(),
  129. // only apply constructor args for WASM+CreateV2 scenario
  130. ...(createV2 && {
  131. constructorArgs: inner
  132. .constructorArgs()
  133. .map((arg) => scValToNative(arg))
  134. }) // empty indicates V2 and no ctor, undefined indicates V1
  135. };
  136. break;
  137. }
  138. // contractExecutableStellarAsset
  139. case 1:
  140. output.args.type = 'sac';
  141. output.args.asset = Asset.fromOperation(
  142. preimage.fromAsset()
  143. ).toString();
  144. break;
  145. default:
  146. throw new Error(`unknown creation type: ${JSON.stringify(exec)}`);
  147. }
  148. break;
  149. }
  150. default:
  151. throw new Error(
  152. `unknown invocation type (${fn.switch()}): ${JSON.stringify(fn)}`
  153. );
  154. }
  155. output.invocations = root.subInvocations().map((i) => buildInvocationTree(i));
  156. return output;
  157. }
  158. /**
  159. * @callback InvocationWalker
  160. *
  161. * @param {xdr.SorobanAuthorizedInvocation} node the currently explored node
  162. * @param {number} depth the depth of the tree this node is occurring at (the
  163. * root starts at a depth of 1)
  164. * @param {xdr.SorobanAuthorizedInvocation} [parent] this `node`s parent node,
  165. * if any (i.e. this doesn't exist at the root)
  166. *
  167. * @returns {boolean|null|void} returning exactly `false` is a hint to stop
  168. * exploring, other values are ignored
  169. */
  170. /**
  171. * Executes a callback function on each node in the tree until stopped.
  172. *
  173. * Nodes are walked in a depth-first order. Returning `false` from the callback
  174. * stops further depth exploration at that node, but it does not stop the walk
  175. * in a "global" view.
  176. *
  177. * @param {xdr.SorobanAuthorizedInvocation} root the tree to explore
  178. * @param {InvocationWalker} callback the callback to execute for each node
  179. * @returns {void}
  180. */
  181. export function walkInvocationTree(root, callback) {
  182. walkHelper(root, 1, callback);
  183. }
  184. function walkHelper(node, depth, callback, parent) {
  185. if (callback(node, depth, parent) === false /* allow void rv */) {
  186. return;
  187. }
  188. node
  189. .subInvocations()
  190. .forEach((i) => walkHelper(i, depth + 1, callback, node));
  191. }