/// <summary> /// This method converts a Neo tx to a Rosetta tx. Obsolete tx type will not be converted. /// Put utxo into operations. Put tx type and type specific info into metadata. /// </summary> /// <param name="neoTx"></param> /// <returns></returns> private Transaction ConvertTx(NeoTransaction neoTx) { Dictionary <string, JObject> metadata = new Dictionary <string, JObject>(); string txType = ""; switch (neoTx.Type) { case TransactionType.ClaimTransaction: var ctx = neoTx as ClaimTransaction; txType = nameof(ClaimTransaction); metadata.Add("claims", new JArray(ctx.Claims.Select(p => p.ToJson()))); break; case TransactionType.ContractTransaction: txType = nameof(ContractTransaction); break; case TransactionType.InvocationTransaction: var itx = neoTx as InvocationTransaction; txType = nameof(InvocationTransaction); metadata.Add("script", itx.Script.ToHexString()); metadata.Add("gas", itx.Gas.ToString()); break; case TransactionType.IssueTransaction: txType = nameof(IssueTransaction); break; case TransactionType.MinerTransaction: var mtx = neoTx as MinerTransaction; txType = nameof(MinerTransaction); metadata.Add("nonce", mtx.Nonce.ToString()); break; case TransactionType.StateTransaction: var stx = neoTx as StateTransaction; txType = nameof(StateTransaction); metadata.Add("descriptors", new JArray(stx.Descriptors.Select(p => p.ToJson()))); break; default: return(null); } metadata.Add("tx_type", txType); // handle utxo transfer Operation[] operations = HandleUtxoTransfer(neoTx); Transaction transaction = new Transaction(new TransactionIdentifier(neoTx.Hash.ToString()), operations, new Metadata(metadata)); return(transaction); }
private Operation[] HandleUtxoTransfer(NeoTransaction tx) { var type = OperationType.Transfer; var status = OperationStatus.OPERATION_STATUS_SUCCESS.Status; // from operations Operation[] fromOperations = new Operation[] { }; for (int i = 0; i < tx.Inputs.Length; i++) { var input = tx.Inputs[i]; var prevTx = Blockchain.Singleton.GetTransaction(input.PrevHash); if (prevTx == null) { throw new Exception("previous tx not found"); } var prevOutput = prevTx.Outputs[input.PrevIndex]; if (prevOutput == null) { throw new Exception("previous tx output not found"); } Operation operation = new Operation(new OperationIdentifier(i), type, status, null, new AccountIdentifier(prevOutput.ScriptHash.ToAddress()), new Amount("-" + prevOutput.Value.ToString(), new Currency(prevOutput.AssetId)), new CoinChange(new CoinIdentifier(input.PrevHash, input.PrevIndex), CoinAction.CoinSpent) ); fromOperations = fromOperations.Append(operation).ToArray(); } // to operations Operation[] toOperations = new Operation[] { }; var fromCount = fromOperations.Length; OperationIdentifier[] relatedOperations = fromCount == 0 ? null : Enumerable.Range(0, fromCount).Select(p => new OperationIdentifier(p)).ToArray(); //foreach (var output in tx.Outputs) for (int i = 0; i < tx.Outputs.Length; i++) { var output = tx.Outputs[i]; Operation operation = new Operation(new OperationIdentifier(i + fromCount), type, status, relatedOperations, new AccountIdentifier(output.ScriptHash.ToAddress()), new Amount(output.Value.ToString(), new Currency(output.AssetId)), new CoinChange(new CoinIdentifier(tx.Hash, i), CoinAction.CoinCreated) ); toOperations = toOperations.Append(operation).ToArray(); } return(fromOperations.Concat(toOperations).ToArray()); }
/// <summary> /// Hash returns the network-specific transaction hash for a signed transaction. /// </summary> /// <param name="request"></param> /// <returns></returns> public JObject ConstructionHash(ConstructionHashRequest request) { NeoTransaction neoTx; try { neoTx = NeoTransaction.DeserializeFrom(request.SignedTransaction.HexToBytes()); } catch (Exception) { return(Error.TX_DESERIALIZE_ERROR.ToJson()); } var hash = neoTx.Hash.ToString(); ConstructionHashResponse response = new ConstructionHashResponse(hash); return(response.ToJson()); }
/// <summary> /// Submit a pre-signed transaction to the node. This call should not block on the transaction being included in a block. /// Rather, it should return immediately with an indication of whether or not the transaction was included in the mempool. /// The transaction submission response should only return a 200 status if the submitted transaction could be included in the mempool. /// Otherwise, it should return an error. /// </summary> /// <param name="request"></param> /// <returns></returns> public JObject ConstructionSubmit(ConstructionSubmitRequest request) { NeoTransaction neoTx; try { neoTx = NeoTransaction.DeserializeFrom(request.SignedTransaction.HexToBytes()); } catch (Exception) { return(Error.TX_DESERIALIZE_ERROR.ToJson()); } RelayResultReason reason = system.Blockchain.Ask <RelayResultReason>(neoTx).Result; switch (reason) { case RelayResultReason.Succeed: TransactionIdentifier transactionIdentifier = new TransactionIdentifier(neoTx.Hash.ToString()); ConstructionSubmitResponse response = new ConstructionSubmitResponse(transactionIdentifier); return(response.ToJson()); case RelayResultReason.AlreadyExists: return(Error.ALREADY_EXISTS.ToJson()); case RelayResultReason.OutOfMemory: return(Error.OUT_OF_MEMORY.ToJson()); case RelayResultReason.UnableToVerify: return(Error.UNABLE_TO_VERIFY.ToJson()); case RelayResultReason.Invalid: return(Error.TX_INVALID.ToJson()); case RelayResultReason.PolicyFail: return(Error.POLICY_FAIL.ToJson()); default: return(Error.UNKNOWN_ERROR.ToJson()); } }
/// <summary> /// Parse is called on both unsigned and signed transactions to understand the intent of the formulated transaction. /// This is run as a sanity check before signing (after `/construction/payloads`) and before broadcast (after `/construction/combine`). /// </summary> /// <param name="request"></param> /// <returns></returns> public JObject ConstructionParse(ConstructionParseRequest request) { NeoTransaction neoTx; try { if (request.Signed) { neoTx = NeoTransaction.DeserializeFrom(request.Transaction.HexToBytes()); } else { byte[] rawTx = request.Transaction.HexToBytes(); TransactionType type = (TransactionType)rawTx[0]; switch (type) { case TransactionType.ClaimTransaction: neoTx = new ClaimTransaction(); break; case TransactionType.ContractTransaction: neoTx = new ContractTransaction(); break; case TransactionType.StateTransaction: neoTx = new StateTransaction(); break; case TransactionType.InvocationTransaction: neoTx = new InvocationTransaction(); break; default: throw new ArgumentException(); } using (MemoryStream ms = new MemoryStream(rawTx, false)) using (BinaryReader br = new BinaryReader(ms, Encoding.UTF8)) { (neoTx as IVerifiable).DeserializeUnsigned(br); } } } catch (Exception) { return(Error.TX_DESERIALIZE_ERROR.ToJson()); } Transaction tx = ConvertTx(neoTx); Operation[] operations = tx.Operations; string[] signers = new string[0]; if (request.Signed) { signers = GetSignersFromWitnesses(neoTx.Witnesses); if (signers is null) { return(Error.TX_WITNESS_INVALID.ToJson()); } } ConstructionParseResponse response = new ConstructionParseResponse(operations, signers, tx.Metadata); return(response.ToJson()); }
/// <summary> /// Get any information required to construct a transaction for a specific network. /// Metadata returned here could be a recent hash to use, an account sequence number, or even arbitrary chain state. /// The request used when calling this endpoint is often created by calling `/construction/preprocess` in an offline environment. /// You should NEVER assume that the request sent to this endpoint will be created by the caller or populated with any custom parameters. This must occur in /construction/preprocess. /// It is important to clarify that this endpoint should not pre-construct any transactions for the client (this should happen in `/construction/payloads`). /// This endpoint is left purposely unstructured because of the wide scope of metadata that could be required. /// </summary> /// <returns></returns> public JObject ConstructionMetadata(ConstructionMetadataRequest request) { if (request.NetworkIdentifier?.Blockchain?.ToLower() != "neo n3") { return(Error.NETWORK_IDENTIFIER_INVALID.ToJson()); } if (request.NetworkIdentifier?.Network?.ToLower() != network) { return(Error.NETWORK_IDENTIFIER_INVALID.ToJson()); } // get signers Signer[] signers = Array.Empty <Signer>(); try { signers = (request.Options["signers"] as JArray).Select(p => p.ToSigner(system.Settings.AddressVersion)).ToArray(); } catch (Exception) { return(Error.PARAMETER_INVALID.ToJson()); } // get public keys, need public keys to calculate network fee if (!request.Options.ContainsKey("signer_metadata")) { return(Error.PARAMETER_INVALID.ToJson()); } SignerMetadata[] signerMetadatas = (request.Options["signer_metadata"] as JArray).Select(p => SignerMetadata.FromJson(p, system.Settings.AddressVersion)).ToArray(); if (request.PublicKeys == null) { return(Error.PARAMETER_INVALID.ToJson()); } // get script byte[] script = Array.Empty <byte>(); if (request.Options.ContainsKey("has_existing_script") && request.Options["has_existing_script"].AsBoolean() == true) { if (request.Options.ContainsKey("script")) { script = Convert.FromBase64String(request.Options["script"].AsString()); } else { return(Error.PARAMETER_INVALID.ToJson()); } } else if (request.Options.ContainsKey("has_existing_script") && request.Options["has_existing_script"].AsBoolean() == false) { if (request.Options.ContainsKey("operations_script")) { script = Convert.FromBase64String(request.Options["operations_script"].AsString()); } else { return(Error.PARAMETER_INVALID.ToJson()); } } else { return(Error.PARAMETER_INVALID.ToJson()); } // get max fee long maxFee = 2000000000; if (request.Options.ContainsKey("max_fee")) { maxFee = long.Parse(request.Options["max_fee"].AsString()); } // invoke script NeoTransaction tx = new NeoTransaction { Signers = signers, Attributes = Array.Empty <TransactionAttribute>(), }; using ApplicationEngine engine = ApplicationEngine.Run(script, system.StoreView, container: tx, settings: system.Settings, gas: maxFee); if (engine.State == VMState.FAULT) { return(Error.VM_FAULT.ToJson()); } // script tx.Script = script; // version var version = (byte)0; tx.Version = version; // nonce Random random = new(); var nonce = (uint)random.Next(); tx.Nonce = nonce; // system fee var sysFee = engine.GasConsumed; tx.SystemFee = sysFee; // network fee is not available here, will be calculated in var netFee = CalculateNetworkFee(system.StoreView, tx, request.PublicKeys, signerMetadatas); // valid until block var validUntilBlock = NativeContract.Ledger.CurrentIndex(system.StoreView) + system.Settings.MaxValidUntilBlockIncrement; Metadata metadata = new(new() { { "version", version }, { "nonce", nonce },