From 85aeb6521714d48a86e7e982224be81546155578 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 10 May 2025 03:05:47 +0700 Subject: [PATCH 1/2] ThirdwebBridge - Onramp Integration & Improved APIs --- Thirdweb.Console/Program.cs | 199 ++++++----- .../ThirdwebBridge.Extensions.cs | 58 +++- .../Thirdweb.Bridge/ThirdwebBridge.Types.cs | 317 +++++++++++++++--- Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs | 152 ++++++++- 4 files changed, 576 insertions(+), 150 deletions(-) diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 50ff598..0d23258 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -45,88 +45,123 @@ #region Bridge -// // Create a ThirdwebBridge instance -// var bridge = await ThirdwebBridge.Create(client); - -// // Buy - Get a quote for buying a specific amount of tokens -// var buyQuote = await bridge.Buy_Quote( -// originChainId: 1, -// originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum -// destinationChainId: 324, -// destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync -// buyAmountWei: BigInteger.Parse("0.1".ToWei()) -// ); -// Console.WriteLine($"Buy quote: {JsonConvert.SerializeObject(buyQuote, Formatting.Indented)}"); - -// // Buy - Get an executable set of transactions (alongside a quote) for buying a specific amount of tokens -// var preparedBuy = await bridge.Buy_Prepare( -// originChainId: 1, -// originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum -// destinationChainId: 324, -// destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync -// buyAmountWei: BigInteger.Parse("0.1".ToWei()), -// sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), -// receiver: await myWallet.GetAddress() -// ); -// Console.WriteLine($"Prepared Buy contains {preparedBuy.Transactions.Count} transaction(s)!"); - -// // Sell - Get a quote for selling a specific amount of tokens -// var sellQuote = await bridge.Sell_Quote( -// originChainId: 324, -// originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync -// destinationChainId: 1, -// destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum -// sellAmountWei: BigInteger.Parse("0.1".ToWei()) -// ); -// Console.WriteLine($"Sell quote: {JsonConvert.SerializeObject(sellQuote, Formatting.Indented)}"); - -// // Sell - Get an executable set of transactions (alongside a quote) for selling a specific amount of tokens -// var preparedSell = await bridge.Sell_Prepare( -// originChainId: 324, -// originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync -// destinationChainId: 1, -// destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum -// sellAmountWei: BigInteger.Parse("0.1".ToWei()), -// sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), -// receiver: await myWallet.GetAddress() -// ); -// Console.WriteLine($"Prepared Sell contains {preparedSell.Transactions.Count} transaction(s)!"); - -// // Transfer - Get an executable transaction for transferring a specific amount of tokens -// var preparedTransfer = await bridge.Transfer_Prepare( -// chainId: 137, -// tokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync -// transferAmountWei: BigInteger.Parse("0.1".ToWei()), -// sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), -// receiver: await myWallet.GetAddress() -// ); -// Console.WriteLine($"Prepared Transfer: {JsonConvert.SerializeObject(preparedTransfer, Formatting.Indented)}"); - -// // You may use our extensions to execute yourself... -// var myTx = await preparedTransfer.Transactions[0].ToThirdwebTransaction(myWallet); -// var myHash = await ThirdwebTransaction.Send(myTx); - -// // ...and poll for the status... -// var status = await bridge.Status(transactionHash: myHash, chainId: 1); -// var isComplete = status.StatusType == StatusType.COMPLETED; -// Console.WriteLine($"Status: {JsonConvert.SerializeObject(status, Formatting.Indented)}"); - -// // Or use our Execute extensions directly to handle everything for you! - -// // Execute a prepared Buy -// var buyResult = await bridge.Execute(myWallet, preparedBuy); -// var buyHashes = buyResult.Select(receipt => receipt.TransactionHash).ToList(); -// Console.WriteLine($"Buy hashes: {JsonConvert.SerializeObject(buyHashes, Formatting.Indented)}"); - -// // Execute a prepared Sell -// var sellResult = await bridge.Execute(myWallet, preparedSell); -// var sellHashes = sellResult.Select(receipt => receipt.TransactionHash).ToList(); -// Console.WriteLine($"Sell hashes: {JsonConvert.SerializeObject(sellHashes, Formatting.Indented)}"); - -// // Execute a prepared Transfer -// var transferResult = await bridge.Execute(myWallet, preparedTransfer); -// var transferHashes = transferResult.Select(receipt => receipt.TransactionHash).ToList(); -// Console.WriteLine($"Transfer hashes: {JsonConvert.SerializeObject(transferHashes, Formatting.Indented)}"); +var myWallet = await PrivateKeyWallet.Generate(client); + +// Create a ThirdwebBridge instance +var bridge = await ThirdwebBridge.Create(client); + +// Buy - Get a quote for buying a specific amount of tokens +var buyQuote = await bridge.Buy_Quote( + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum + destinationChainId: 324, + destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync + buyAmountWei: BigInteger.Parse("0.01".ToWei()) +); +Console.WriteLine($"Buy quote: {JsonConvert.SerializeObject(buyQuote, Formatting.Indented)}"); + +// Buy - Get an executable set of transactions (alongside a quote) for buying a specific amount of tokens +var preparedBuy = await bridge.Buy_Prepare( + originChainId: 1, + originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum + destinationChainId: 324, + destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync + buyAmountWei: BigInteger.Parse("0.01".ToWei()), + sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), + receiver: await myWallet.GetAddress() +); +Console.WriteLine($"Prepared Buy contains {preparedBuy.Steps.Count} steps(s) with a total of {preparedBuy.Steps.Sum(step => step.Transactions.Count)} transactions!"); + +// Sell - Get a quote for selling a specific amount of tokens +var sellQuote = await bridge.Sell_Quote( + originChainId: 324, + originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync + destinationChainId: 1, + destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum + sellAmountWei: BigInteger.Parse("0.01".ToWei()) +); +Console.WriteLine($"Sell quote: {JsonConvert.SerializeObject(sellQuote, Formatting.Indented)}"); + +// Sell - Get an executable set of transactions (alongside a quote) for selling a specific amount of tokens +var preparedSell = await bridge.Sell_Prepare( + originChainId: 324, + originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync + destinationChainId: 1, + destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum + sellAmountWei: BigInteger.Parse("0.01".ToWei()), + sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), + receiver: await myWallet.GetAddress() +); +Console.WriteLine($"Prepared Sell contains {preparedBuy.Steps.Count} steps(s) with a total of {preparedBuy.Steps.Sum(step => step.Transactions.Count)} transactions!"); + +// Transfer - Get an executable transaction for transferring a specific amount of tokens +var preparedTransfer = await bridge.Transfer_Prepare( + chainId: 137, + tokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // POL on Polygon + transferAmountWei: BigInteger.Parse("0.01".ToWei()), + sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), + receiver: await myWallet.GetAddress() +); +Console.WriteLine($"Prepared Transfer: {JsonConvert.SerializeObject(preparedTransfer, Formatting.Indented)}"); + +// You may use our extensions to execute yourself... +var myTx = await preparedTransfer.Transactions[0].ToThirdwebTransaction(myWallet); +var myHash = await ThirdwebTransaction.Send(myTx); + +// ...and poll for the status... +var status = await bridge.Status(transactionHash: myHash, chainId: 1); +var isComplete = status.StatusType == StatusType.COMPLETED; +Console.WriteLine($"Status: {JsonConvert.SerializeObject(status, Formatting.Indented)}"); + +// Or use our Execute extensions directly to handle everything for you! + +// Execute a prepared Buy +var buyResult = await bridge.Execute(myWallet, preparedBuy); +var buyHashes = buyResult.Select(receipt => receipt.TransactionHash).ToList(); +Console.WriteLine($"Buy hashes: {JsonConvert.SerializeObject(buyHashes, Formatting.Indented)}"); + +// Execute a prepared Sell +var sellResult = await bridge.Execute(myWallet, preparedSell); +var sellHashes = sellResult.Select(receipt => receipt.TransactionHash).ToList(); +Console.WriteLine($"Sell hashes: {JsonConvert.SerializeObject(sellHashes, Formatting.Indented)}"); + +// Execute a prepared Transfer +var transferResult = await bridge.Execute(myWallet, preparedTransfer); +var transferHashes = transferResult.Select(receipt => receipt.TransactionHash).ToList(); +Console.WriteLine($"Transfer hashes: {JsonConvert.SerializeObject(transferHashes, Formatting.Indented)}"); + +// Onramp - Get a quote for buying crypto with Fiat +var preparedOnramp = await bridge.Onramp_Prepare( + onramp: OnrampProvider.Coinbase, + chainId: 8453, + tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base + amount: "10000000", + receiver: await myWallet.GetAddress() +); +Console.WriteLine($"Onramp link: {preparedOnramp.Link}"); +Console.WriteLine($"Full onramp quote and steps data: {JsonConvert.SerializeObject(preparedOnramp, Formatting.Indented)}"); + +while (true) +{ + var onrampStatus = await bridge.Onramp_Status(id: preparedOnramp.Id); + Console.WriteLine($"Full Onramp Status: {JsonConvert.SerializeObject(onrampStatus, Formatting.Indented)}"); + if (onrampStatus.StatusType is StatusType.COMPLETED or StatusType.FAILED) + { + break; + } + await ThirdwebTask.Delay(5000); +} + +if (preparedOnramp.IsSwapRequiredPostOnramp()) +{ + // Execute additional steps that are required post-onramp to get to your token, manually or via the Execute extension + var receipts = await bridge.Execute(myWallet, preparedOnramp); + Console.WriteLine($"Onramp receipts: {JsonConvert.SerializeObject(receipts, Formatting.Indented)}"); +} +else +{ + Console.WriteLine("No additional steps required post-onramp, you can use the tokens directly!"); +} #endregion diff --git a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Extensions.cs b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Extensions.cs index 04d1428..aa231a8 100644 --- a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Extensions.cs +++ b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Extensions.cs @@ -16,7 +16,7 @@ public static class ThirdwebBridgeExtensions /// The transaction receipts as a list of . public static async Task> Execute(this ThirdwebBridge bridge, IThirdwebWallet executor, BuyPrepareData preparedBuy, CancellationToken cancellationToken = default) { - return await ExecuteInternal(bridge, executor, preparedBuy.Transactions, cancellationToken); + return await ExecuteInternal(bridge, executor, preparedBuy.Steps, cancellationToken); } /// @@ -34,7 +34,7 @@ public static async Task> Execute( CancellationToken cancellationToken = default ) { - return await ExecuteInternal(bridge, executor, preparedSell.Transactions, cancellationToken); + return await ExecuteInternal(bridge, executor, preparedSell.Steps, cancellationToken); } /// @@ -52,23 +52,48 @@ public static Task> Execute( CancellationToken cancellationToken = default ) { - return ExecuteInternal(bridge, executor, preparedTransfer.Transactions, cancellationToken); + var steps = new List() { new() { Transactions = preparedTransfer.Transactions } }; + return ExecuteInternal(bridge, executor, steps, cancellationToken); } - private static async Task> ExecuteInternal( - this ThirdwebBridge bridge, - IThirdwebWallet executor, - List transactions, - CancellationToken cancellationToken = default - ) + /// + /// Executes a set of post-onramp transactions and handles status polling. + /// + /// The Thirdweb bridge. + /// The executor wallet. + /// The prepared onramp data. + /// The cancellation token. + /// The transaction receipts as a list of . + /// Note: This method is used for executing transactions after an onramp process. + public static Task> Execute(this ThirdwebBridge bridge, IThirdwebWallet executor, OnrampPrepareData preparedOnRamp, CancellationToken cancellationToken = default) + { + return ExecuteInternal(bridge, executor, preparedOnRamp.Steps, cancellationToken); + } + + /// + /// Executes a set of transactions and handles status polling. + /// + /// /// The Thirdweb bridge. + /// The executor wallet. + /// The steps containing transactions to execute. + /// The cancellation token. + public static Task> Execute(this ThirdwebBridge bridge, IThirdwebWallet executor, List steps, CancellationToken cancellationToken = default) + { + return ExecuteInternal(bridge, executor, steps, cancellationToken); + } + + private static async Task> ExecuteInternal(this ThirdwebBridge bridge, IThirdwebWallet executor, List steps, CancellationToken cancellationToken = default) { var receipts = new List(); - foreach (var tx in transactions) + foreach (var step in steps) { - var thirdwebTx = await tx.ToThirdwebTransaction(executor); - var hash = await ThirdwebTransaction.Send(thirdwebTx); - receipts.Add(await ThirdwebTransaction.WaitForTransactionReceipt(executor.Client, tx.ChainId, hash, cancellationToken)); - _ = await bridge.WaitForStatusCompletion(hash, tx.ChainId, cancellationToken); + foreach (var tx in step.Transactions) + { + var thirdwebTx = await tx.ToThirdwebTransaction(executor); + var hash = await ThirdwebTransaction.Send(thirdwebTx); + receipts.Add(await ThirdwebTransaction.WaitForTransactionReceipt(executor.Client, tx.ChainId, hash, cancellationToken)); + _ = await bridge.WaitForStatusCompletion(hash, tx.ChainId, cancellationToken); + } } return receipts; } @@ -117,5 +142,10 @@ public static async Task WaitForStatusCompletion(this ThirdwebBridge return status; } + public static bool IsSwapRequiredPostOnramp(this OnrampPrepareData preparedOnramp) + { + return preparedOnramp.Steps == null || preparedOnramp.Steps.Count == 0 || !preparedOnramp.Steps.Any(step => step.Transactions?.Count > 0); + } + #endregion } diff --git a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Types.cs b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Types.cs index a5a7219..368bef6 100644 --- a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Types.cs +++ b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.Types.cs @@ -24,40 +24,46 @@ internal class ResponseModel public class Intent { /// - /// The chain ID where the transaction originates. + /// The origin chain ID. /// [JsonProperty("originChainId")] public BigInteger OriginChainId { get; set; } /// - /// The token address in the origin chain. + /// The origin token address. /// [JsonProperty("originTokenAddress")] public string OriginTokenAddress { get; set; } /// - /// The chain ID where the transaction is executed. + /// The destination chain ID. /// [JsonProperty("destinationChainId")] public BigInteger DestinationChainId { get; set; } /// - /// The token address in the destination chain. + /// The destination token address. /// [JsonProperty("destinationTokenAddress")] public string DestinationTokenAddress { get; set; } /// - /// The amount involved in the transaction (buy, sell, or transfer) in wei. + /// The desired amount in wei. /// - public virtual string AmountWei { get; set; } + [JsonProperty("amount")] + public string Amount { get; set; } + + /// + /// The maximum number of steps in the returned route (optional). + /// + [JsonProperty("maxSteps", NullValueHandling = NullValueHandling.Ignore)] + public int? MaxSteps { get; set; } = 3; } /// /// Represents the common fields for both Buy and Sell transactions. /// -public class QuoteData - where TIntent : Intent +public class QuoteData { /// /// The amount (in wei) of the input token that must be paid to receive the desired amount. @@ -93,14 +99,80 @@ public class QuoteData /// The intent object containing details about the transaction. /// [JsonProperty("intent")] - public TIntent Intent { get; set; } + public Intent Intent { get; set; } + + [JsonProperty("steps")] + public List Steps { get; set; } + + [JsonProperty("purchaseData", NullValueHandling = NullValueHandling.Ignore)] + public object PurchaseData { get; set; } +} + +/// +/// Represents a single step in a transaction, including origin and destination tokens. +/// +public class Step +{ + [JsonProperty("originToken")] + public TokenData OriginToken { get; set; } + + [JsonProperty("destinationToken")] + public TokenData DestinationToken { get; set; } + + [JsonProperty("transactions")] + public List Transactions { get; set; } + + [JsonProperty("originAmount")] + public string OriginAmount { get; set; } + + [JsonProperty("destinationAmount")] + public string DestinationAmount { get; set; } + + [JsonProperty("nativeFee")] + public string NativeFee { get; set; } + + [JsonProperty("estimatedExecutionTimeMs")] + public long EstimatedExecutionTimeMs { get; set; } } /// -/// Represents a transaction to be executed. +/// Represents a token in a step, including metadata like chain ID, address, and pricing. +/// +public class TokenData +{ + [JsonProperty("chainId")] + public BigInteger ChainId { get; set; } + + [JsonProperty("address")] + public string Address { get; set; } + + [JsonProperty("symbol")] + public string Symbol { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("decimals")] + public int Decimals { get; set; } + + [JsonProperty("priceUsd")] + public decimal PriceUsd { get; set; } + + [JsonProperty("iconUri")] + public string IconUri { get; set; } +} + +/// +/// Represents a transaction ready to be executed. /// public class Transaction { + /// + /// The transaction ID, each step in a quoted payment will have a unique transaction ID. + /// + [JsonProperty("id")] + public string Id { get; set; } + /// /// The chain ID where the transaction will take place. /// @@ -108,17 +180,41 @@ public class Transaction public BigInteger ChainId { get; set; } /// - /// The address to which the transaction is sent, or null if not applicable. + /// The maximum priority fee per gas (EIP-1559). + /// + [JsonProperty("maxPriorityFeePerGas", NullValueHandling = NullValueHandling.Ignore)] + public string MaxPriorityFeePerGas { get; set; } + + /// + /// The maximum fee per gas (EIP-1559). /// - [JsonProperty("to", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("maxFeePerGas", NullValueHandling = NullValueHandling.Ignore)] + public string MaxFeePerGas { get; set; } + + /// + /// The address to which the transaction is sent. + /// + [JsonProperty("to")] public string To { get; set; } + /// + /// The address from which the transaction is sent, or null if not applicable. + /// + [JsonProperty("from", NullValueHandling = NullValueHandling.Ignore)] + public string From { get; set; } + /// /// The value (amount) to be sent in the transaction. /// - [JsonProperty("value")] + [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] public string Value { get; set; } + /// + /// The gas limit for the transaction. + /// + [JsonProperty("gas", NullValueHandling = NullValueHandling.Ignore)] + public string Gas { get; set; } + /// /// The transaction data. /// @@ -130,6 +226,12 @@ public class Transaction /// [JsonProperty("type")] public string Type { get; set; } + + /// + /// The action type for the transaction (e.g., "approval", "transfer", "buy", "sell"). + /// + [JsonProperty("action")] + public string Action { get; set; } } #endregion @@ -139,16 +241,23 @@ public class Transaction /// /// Represents the data returned in the buy quote response. /// -public class BuyQuoteData : QuoteData { } +public class BuyQuoteData : QuoteData { } /// /// Represents the data returned in the buy prepare response. /// -public class BuyPrepareData : QuoteData +public class BuyPrepareData : QuoteData { + /// + /// A hex ID associated with the quoted payment. + /// + [JsonProperty("id")] + public string Id { get; set; } + /// /// An array of transactions to be executed to fulfill this quote (in order). /// + [Obsolete("Use Steps.Transactions instead.")] [JsonProperty("transactions")] public List Transactions { get; set; } @@ -159,18 +268,6 @@ public class BuyPrepareData : QuoteData public long? Expiration { get; set; } } -/// -/// Represents the intent object for a buy quote. -/// -public class BuyIntent : Intent -{ - /// - /// The desired output amount in wei for buying. - /// - [JsonProperty("buyAmountWei")] - public override string AmountWei { get; set; } -} - #endregion #region Sell @@ -178,16 +275,23 @@ public class BuyIntent : Intent /// /// Represents the data returned in the sell quote response. /// -public class SellQuoteData : QuoteData { } +public class SellQuoteData : QuoteData { } /// /// Represents the data returned in the sell prepare response. /// -public class SellPrepareData : QuoteData +public class SellPrepareData : QuoteData { + /// + /// A hex ID associated with the quoted payment. + /// + [JsonProperty("id")] + public string Id { get; set; } + /// /// An array of transactions to be executed to fulfill this quote (in order). /// + [Obsolete("Use Steps.Transactions instead.")] [JsonProperty("transactions")] public List Transactions { get; set; } @@ -198,18 +302,6 @@ public class SellPrepareData : QuoteData public long? Expiration { get; set; } } -/// -/// Represents the intent object for a sell quote. -/// -public class SellIntent : Intent -{ - /// - /// The amount to sell in wei. - /// - [JsonProperty("sellAmountWei")] - public override string AmountWei { get; set; } -} - #endregion #region Transfer @@ -234,6 +326,9 @@ public class TransferPrepareData [JsonProperty("estimatedExecutionTimeMs")] public long EstimatedExecutionTimeMs { get; set; } + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("transactions")] public List Transactions { get; set; } @@ -250,7 +345,7 @@ public class TransferPrepareData public class TransferIntent { [JsonProperty("chainId")] - public int ChainId { get; set; } + public BigInteger ChainId { get; set; } [JsonProperty("tokenAddress")] public string TokenAddress { get; set; } @@ -263,6 +358,12 @@ public class TransferIntent [JsonProperty("receiver")] public string Receiver { get; set; } + + [JsonProperty("feePayer")] + public string FeePayer { get; set; } = "sender"; + + [JsonProperty("purchaseData", NullValueHandling = NullValueHandling.Ignore)] + public object PurchaseData { get; set; } } #endregion @@ -277,7 +378,10 @@ public enum StatusType FAILED, PENDING, COMPLETED, - NOT_FOUND + NOT_FOUND, + PROCESSING, + CREATED, + UNKNOWN } /// @@ -296,7 +400,7 @@ public class StatusData "PENDING" => StatusType.PENDING, "COMPLETED" => StatusType.COMPLETED, "NOT_FOUND" => StatusType.NOT_FOUND, - _ => throw new InvalidOperationException($"Unknown status: {this.Status}") + _ => StatusType.UNKNOWN }; /// @@ -311,6 +415,18 @@ public class StatusData [JsonProperty("transactions")] public List Transactions { get; set; } + /// + /// The unique payment ID for the transaction. + /// + [JsonProperty("paymentId", NullValueHandling = NullValueHandling.Ignore)] + public string PaymentId { get; set; } + + /// + /// The unique transaction ID for the transaction. + /// + [JsonProperty("transactionId", NullValueHandling = NullValueHandling.Ignore)] + public string TransactionId { get; set; } + /// /// The origin chain ID (for PENDING and COMPLETED statuses). /// @@ -346,6 +462,12 @@ public class StatusData /// [JsonProperty("destinationAmount", NullValueHandling = NullValueHandling.Ignore)] public string DestinationAmount { get; set; } + + /// + /// The purchase data, which can be null. + /// + [JsonProperty("purchaseData", NullValueHandling = NullValueHandling.Ignore)] + public object PurchaseData { get; set; } } /// @@ -367,3 +489,110 @@ public class TransactionStatus } #endregion + +#region Onramp + +public enum OnrampProvider +{ + Stripe, + Coinbase, + Transak +} + +/// +/// Represents the core data of an onramp response. +/// +public class OnrampPrepareData +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("link")] + public string Link { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("currencyAmount")] + public decimal CurrencyAmount { get; set; } + + [JsonProperty("destinationAmount")] + public string DestinationAmount { get; set; } + + [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] + public long? Timestamp { get; set; } + + [JsonProperty("expiration", NullValueHandling = NullValueHandling.Ignore)] + public long? Expiration { get; set; } + + [JsonProperty("steps")] + public List Steps { get; set; } + + [JsonProperty("intent")] + public OnrampIntent Intent { get; set; } +} + +/// +/// Represents the intent used to prepare the onramp. +/// +public class OnrampIntent +{ + [JsonProperty("onramp")] + public OnrampProvider Onramp { get; set; } + + [JsonProperty("chainId")] + public BigInteger ChainId { get; set; } + + [JsonProperty("tokenAddress")] + public string TokenAddress { get; set; } + + [JsonProperty("amount")] + public string Amount { get; set; } + + [JsonProperty("receiver")] + public string Receiver { get; set; } + + [JsonProperty("purchaseData", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary PurchaseData { get; set; } + + [JsonProperty("onrampTokenAddress", NullValueHandling = NullValueHandling.Ignore)] + public string OnrampTokenAddress { get; set; } + + [JsonProperty("onrampChainId", NullValueHandling = NullValueHandling.Ignore)] + public BigInteger? OnrampChainId { get; set; } + + [JsonProperty("currency", NullValueHandling = NullValueHandling.Ignore)] + public string Currency { get; set; } = "USD"; + + [JsonProperty("maxSteps", NullValueHandling = NullValueHandling.Ignore)] + public int? MaxSteps { get; set; } = 3; + + [JsonProperty("excludeChainIds", NullValueHandling = NullValueHandling.Ignore)] + public List ExcludeChainIds { get; set; } +} + +/// +/// Represents the status of an onramp transaction. +/// +public class OnrampStatusData +{ + [JsonIgnore] + public StatusType StatusType => + this.Status switch + { + "FAILED" => StatusType.FAILED, + "PENDING" => StatusType.PENDING, + "COMPLETED" => StatusType.COMPLETED, + "PROCESSING" => StatusType.PROCESSING, + "CREATED" => StatusType.CREATED, + _ => StatusType.UNKNOWN + }; + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("transactionHash")] + public string TransactionHash { get; set; } +} + +#endregion diff --git a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs index 095fd8f..331a6f7 100644 --- a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs +++ b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs @@ -32,9 +32,17 @@ public static Task Create(ThirdwebClient client) /// The chain ID of the destination chain. /// The address of the token on the destination chain. /// The amount of tokens to buy in wei. + /// The maximum number of steps in the returned route. /// A object representing the quote. /// Thrown when one of the parameters is invalid. - public async Task Buy_Quote(BigInteger originChainId, string originTokenAddress, BigInteger destinationChainId, string destinationTokenAddress, BigInteger buyAmountWei) + public async Task Buy_Quote( + BigInteger originChainId, + string originTokenAddress, + BigInteger destinationChainId, + string destinationTokenAddress, + BigInteger buyAmountWei, + int maxSteps = 3 + ) { if (originChainId <= 0) { @@ -63,7 +71,8 @@ public async Task Buy_Quote(BigInteger originChainId, string origi { "originTokenAddress", originTokenAddress }, { "destinationChainId", destinationChainId.ToString() }, { "destinationTokenAddress", destinationTokenAddress }, - { "buyAmountWei", buyAmountWei.ToString() } + { "buyAmountWei", buyAmountWei.ToString() }, + { "maxSteps", maxSteps.ToString() } }; url = AppendQueryParams(url, queryParams); @@ -84,6 +93,8 @@ public async Task Buy_Quote(BigInteger originChainId, string origi /// The amount of tokens to buy in wei. /// The address of the sender. /// The address of the receiver. + /// The maximum number of steps in the returned route. + /// Arbitrary purchase data to be included with the payment and returned with all webhooks and status checks. /// A object representing the prepare data. /// Thrown when one of the parameters is invalid. public async Task Buy_Prepare( @@ -93,7 +104,9 @@ public async Task Buy_Prepare( string destinationTokenAddress, BigInteger buyAmountWei, string sender, - string receiver + string receiver, + int maxSteps = 3, + object purchaseData = null ) { if (originChainId <= 0) @@ -135,7 +148,9 @@ string receiver { "destinationTokenAddress", destinationTokenAddress }, { "buyAmountWei", buyAmountWei.ToString() }, { "sender", sender }, - { "receiver", receiver } + { "receiver", receiver }, + { "maxSteps", maxSteps.ToString() }, + { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null } }; url = AppendQueryParams(url, queryParams); @@ -158,9 +173,17 @@ string receiver /// The chain ID of the destination chain. /// The address of the token on the destination chain. /// The amount of tokens to sell in wei. + /// The maximum number of steps in the returned route. /// A object representing the quote. /// Thrown when one of the parameters is invalid. - public async Task Sell_Quote(BigInteger originChainId, string originTokenAddress, BigInteger destinationChainId, string destinationTokenAddress, BigInteger sellAmountWei) + public async Task Sell_Quote( + BigInteger originChainId, + string originTokenAddress, + BigInteger destinationChainId, + string destinationTokenAddress, + BigInteger sellAmountWei, + int maxSteps = 3 + ) { if (originChainId <= 0) { @@ -189,7 +212,8 @@ public async Task Sell_Quote(BigInteger originChainId, string ori { "originTokenAddress", originTokenAddress }, { "destinationChainId", destinationChainId.ToString() }, { "destinationTokenAddress", destinationTokenAddress }, - { "sellAmountWei", sellAmountWei.ToString() } + { "sellAmountWei", sellAmountWei.ToString() }, + { "maxSteps", maxSteps.ToString() } }; url = AppendQueryParams(url, queryParams); @@ -210,6 +234,8 @@ public async Task Sell_Quote(BigInteger originChainId, string ori /// The amount of tokens to sell in wei. /// The address of the sender. /// The address of the receiver. + /// The maximum number of steps in the returned route. + /// Arbitrary purchase data to be included with the payment and returned with all webhooks and status checks. /// A object representing the prepare data. /// Thrown when one of the parameters is invalid. public async Task Sell_Prepare( @@ -219,7 +245,9 @@ public async Task Sell_Prepare( string destinationTokenAddress, BigInteger sellAmountWei, string sender, - string receiver + string receiver, + int maxSteps = 3, + object purchaseData = null ) { if (originChainId <= 0) @@ -261,7 +289,9 @@ string receiver { "destinationTokenAddress", destinationTokenAddress }, { "sellAmountWei", sellAmountWei.ToString() }, { "sender", sender }, - { "receiver", receiver } + { "receiver", receiver }, + { "maxSteps", maxSteps.ToString() }, + { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null } }; url = AppendQueryParams(url, queryParams); @@ -284,9 +314,19 @@ string receiver /// The amount of tokens to transfer in wei. /// The address of the sender. /// The address of the receiver. + /// The fee payer (default is "sender"). + /// Arbitrary purchase data to be included with the payment and returned with all webhooks and status checks. /// A object representing the prepare data. /// Thrown when one of the parameters is invalid. - public async Task Transfer_Prepare(BigInteger chainId, string tokenAddress, BigInteger transferAmountWei, string sender, string receiver) + public async Task Transfer_Prepare( + BigInteger chainId, + string tokenAddress, + BigInteger transferAmountWei, + string sender, + string receiver, + string feePayer = "sender", + object purchaseData = null + ) { if (chainId <= 0) { @@ -320,7 +360,9 @@ public async Task Transfer_Prepare(BigInteger chainId, stri { "tokenAddress", tokenAddress }, { "transferAmountWei", transferAmountWei.ToString() }, { "sender", sender }, - { "receiver", receiver } + { "receiver", receiver }, + { "feePayer", feePayer }, + { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null } }; url = AppendQueryParams(url, queryParams); @@ -333,6 +375,92 @@ public async Task Transfer_Prepare(BigInteger chainId, stri #endregion + #region Onramp + + public async Task Onramp_Prepare( + OnrampProvider onramp, + BigInteger chainId, + string tokenAddress, + string amount, + string receiver, + string onrampTokenAddress = null, + BigInteger? onrampChainId = null, + string currency = "USD", + int? maxSteps = 3, + List excludeChainIds = null, + object purchaseData = null + ) + { + if (chainId <= 0) + { + throw new ArgumentException("chainId cannot be less than or equal to 0", nameof(chainId)); + } + + if (!Utils.IsValidAddress(tokenAddress)) + { + throw new ArgumentException("tokenAddress is not a valid address", nameof(tokenAddress)); + } + + if (string.IsNullOrWhiteSpace(amount)) + { + throw new ArgumentException("amount cannot be null or empty", nameof(amount)); + } + + if (!Utils.IsValidAddress(receiver)) + { + throw new ArgumentException("receiver is not a valid address", nameof(receiver)); + } + + var url = $"{Constants.BRIDGE_API_URL}/v1/onramp/prepare"; + var queryParams = new Dictionary + { + { "onramp", onramp.ToString().ToLower() }, + { "chainId", chainId.ToString() }, + { "tokenAddress", tokenAddress }, + { "amount", amount }, + { "receiver", receiver }, + { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null }, + { "onrampTokenAddress", onrampTokenAddress }, + { "onrampChainId", onrampChainId?.ToString() }, + { "currency", currency }, + { "maxSteps", maxSteps?.ToString() } + }; + + if (excludeChainIds != null && excludeChainIds.Count > 0) + { + queryParams.Add("excludeChainIds", string.Join(",", excludeChainIds)); + } + + url = AppendQueryParams(url, queryParams); + + var response = await this._httpClient.GetAsync(url).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var result = JsonConvert.DeserializeObject>(responseContent); + return result.Data; + } + + public async Task Onramp_Status(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("id cannot be null or empty", nameof(id)); + } + + var url = $"{Constants.BRIDGE_API_URL}/v1/onramp/status"; + var queryParams = new Dictionary { { "id", id } }; + url = AppendQueryParams(url, queryParams); + + var response = await this._httpClient.GetAsync(url).ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var result = JsonConvert.DeserializeObject>(responseContent); + return result.Data; + } + + #endregion + #region Status /// @@ -372,6 +500,10 @@ private static string AppendQueryParams(string url, Dictionary q var query = new List(); foreach (var param in queryParams) { + if (string.IsNullOrEmpty(param.Value)) + { + continue; + } query.Add($"{param.Key}={param.Value}"); } From 151f4744f9b032ef2758948b66d38a7143307544 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 10 May 2025 03:18:24 +0700 Subject: [PATCH 2/2] GET -> POST on /prepare --- Thirdweb.Console/Program.cs | 294 ++++++++------------- Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs | 125 +++++---- 2 files changed, 185 insertions(+), 234 deletions(-) diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 0d23258..cf240f5 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -45,123 +45,121 @@ #region Bridge -var myWallet = await PrivateKeyWallet.Generate(client); - -// Create a ThirdwebBridge instance -var bridge = await ThirdwebBridge.Create(client); - -// Buy - Get a quote for buying a specific amount of tokens -var buyQuote = await bridge.Buy_Quote( - originChainId: 1, - originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum - destinationChainId: 324, - destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync - buyAmountWei: BigInteger.Parse("0.01".ToWei()) -); -Console.WriteLine($"Buy quote: {JsonConvert.SerializeObject(buyQuote, Formatting.Indented)}"); - -// Buy - Get an executable set of transactions (alongside a quote) for buying a specific amount of tokens -var preparedBuy = await bridge.Buy_Prepare( - originChainId: 1, - originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum - destinationChainId: 324, - destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync - buyAmountWei: BigInteger.Parse("0.01".ToWei()), - sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), - receiver: await myWallet.GetAddress() -); -Console.WriteLine($"Prepared Buy contains {preparedBuy.Steps.Count} steps(s) with a total of {preparedBuy.Steps.Sum(step => step.Transactions.Count)} transactions!"); - -// Sell - Get a quote for selling a specific amount of tokens -var sellQuote = await bridge.Sell_Quote( - originChainId: 324, - originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync - destinationChainId: 1, - destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum - sellAmountWei: BigInteger.Parse("0.01".ToWei()) -); -Console.WriteLine($"Sell quote: {JsonConvert.SerializeObject(sellQuote, Formatting.Indented)}"); - -// Sell - Get an executable set of transactions (alongside a quote) for selling a specific amount of tokens -var preparedSell = await bridge.Sell_Prepare( - originChainId: 324, - originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync - destinationChainId: 1, - destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum - sellAmountWei: BigInteger.Parse("0.01".ToWei()), - sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), - receiver: await myWallet.GetAddress() -); -Console.WriteLine($"Prepared Sell contains {preparedBuy.Steps.Count} steps(s) with a total of {preparedBuy.Steps.Sum(step => step.Transactions.Count)} transactions!"); - -// Transfer - Get an executable transaction for transferring a specific amount of tokens -var preparedTransfer = await bridge.Transfer_Prepare( - chainId: 137, - tokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // POL on Polygon - transferAmountWei: BigInteger.Parse("0.01".ToWei()), - sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), - receiver: await myWallet.GetAddress() -); -Console.WriteLine($"Prepared Transfer: {JsonConvert.SerializeObject(preparedTransfer, Formatting.Indented)}"); - -// You may use our extensions to execute yourself... -var myTx = await preparedTransfer.Transactions[0].ToThirdwebTransaction(myWallet); -var myHash = await ThirdwebTransaction.Send(myTx); - -// ...and poll for the status... -var status = await bridge.Status(transactionHash: myHash, chainId: 1); -var isComplete = status.StatusType == StatusType.COMPLETED; -Console.WriteLine($"Status: {JsonConvert.SerializeObject(status, Formatting.Indented)}"); - -// Or use our Execute extensions directly to handle everything for you! - -// Execute a prepared Buy -var buyResult = await bridge.Execute(myWallet, preparedBuy); -var buyHashes = buyResult.Select(receipt => receipt.TransactionHash).ToList(); -Console.WriteLine($"Buy hashes: {JsonConvert.SerializeObject(buyHashes, Formatting.Indented)}"); - -// Execute a prepared Sell -var sellResult = await bridge.Execute(myWallet, preparedSell); -var sellHashes = sellResult.Select(receipt => receipt.TransactionHash).ToList(); -Console.WriteLine($"Sell hashes: {JsonConvert.SerializeObject(sellHashes, Formatting.Indented)}"); - -// Execute a prepared Transfer -var transferResult = await bridge.Execute(myWallet, preparedTransfer); -var transferHashes = transferResult.Select(receipt => receipt.TransactionHash).ToList(); -Console.WriteLine($"Transfer hashes: {JsonConvert.SerializeObject(transferHashes, Formatting.Indented)}"); - -// Onramp - Get a quote for buying crypto with Fiat -var preparedOnramp = await bridge.Onramp_Prepare( - onramp: OnrampProvider.Coinbase, - chainId: 8453, - tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base - amount: "10000000", - receiver: await myWallet.GetAddress() -); -Console.WriteLine($"Onramp link: {preparedOnramp.Link}"); -Console.WriteLine($"Full onramp quote and steps data: {JsonConvert.SerializeObject(preparedOnramp, Formatting.Indented)}"); - -while (true) -{ - var onrampStatus = await bridge.Onramp_Status(id: preparedOnramp.Id); - Console.WriteLine($"Full Onramp Status: {JsonConvert.SerializeObject(onrampStatus, Formatting.Indented)}"); - if (onrampStatus.StatusType is StatusType.COMPLETED or StatusType.FAILED) - { - break; - } - await ThirdwebTask.Delay(5000); -} - -if (preparedOnramp.IsSwapRequiredPostOnramp()) -{ - // Execute additional steps that are required post-onramp to get to your token, manually or via the Execute extension - var receipts = await bridge.Execute(myWallet, preparedOnramp); - Console.WriteLine($"Onramp receipts: {JsonConvert.SerializeObject(receipts, Formatting.Indented)}"); -} -else -{ - Console.WriteLine("No additional steps required post-onramp, you can use the tokens directly!"); -} +// // Create a ThirdwebBridge instance +// var bridge = await ThirdwebBridge.Create(client); + +// // Buy - Get a quote for buying a specific amount of tokens +// var buyQuote = await bridge.Buy_Quote( +// originChainId: 1, +// originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum +// destinationChainId: 324, +// destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync +// buyAmountWei: BigInteger.Parse("0.01".ToWei()) +// ); +// Console.WriteLine($"Buy quote: {JsonConvert.SerializeObject(buyQuote, Formatting.Indented)}"); + +// // Buy - Get an executable set of transactions (alongside a quote) for buying a specific amount of tokens +// var preparedBuy = await bridge.Buy_Prepare( +// originChainId: 1, +// originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum +// destinationChainId: 324, +// destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync +// buyAmountWei: BigInteger.Parse("0.01".ToWei()), +// sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), +// receiver: await myWallet.GetAddress() +// ); +// Console.WriteLine($"Prepared Buy contains {preparedBuy.Steps.Count} steps(s) with a total of {preparedBuy.Steps.Sum(step => step.Transactions.Count)} transactions!"); + +// // Sell - Get a quote for selling a specific amount of tokens +// var sellQuote = await bridge.Sell_Quote( +// originChainId: 324, +// originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync +// destinationChainId: 1, +// destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum +// sellAmountWei: BigInteger.Parse("0.01".ToWei()) +// ); +// Console.WriteLine($"Sell quote: {JsonConvert.SerializeObject(sellQuote, Formatting.Indented)}"); + +// // Sell - Get an executable set of transactions (alongside a quote) for selling a specific amount of tokens +// var preparedSell = await bridge.Sell_Prepare( +// originChainId: 324, +// originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync +// destinationChainId: 1, +// destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum +// sellAmountWei: BigInteger.Parse("0.01".ToWei()), +// sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), +// receiver: await myWallet.GetAddress() +// ); +// Console.WriteLine($"Prepared Sell contains {preparedBuy.Steps.Count} steps(s) with a total of {preparedBuy.Steps.Sum(step => step.Transactions.Count)} transactions!"); + +// // Transfer - Get an executable transaction for transferring a specific amount of tokens +// var preparedTransfer = await bridge.Transfer_Prepare( +// chainId: 137, +// tokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // POL on Polygon +// transferAmountWei: BigInteger.Parse("0.01".ToWei()), +// sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), +// receiver: await myWallet.GetAddress() +// ); +// Console.WriteLine($"Prepared Transfer: {JsonConvert.SerializeObject(preparedTransfer, Formatting.Indented)}"); + +// // You may use our extensions to execute yourself... +// var myTx = await preparedTransfer.Transactions[0].ToThirdwebTransaction(myWallet); +// var myHash = await ThirdwebTransaction.Send(myTx); + +// // ...and poll for the status... +// var status = await bridge.Status(transactionHash: myHash, chainId: 1); +// var isComplete = status.StatusType == StatusType.COMPLETED; +// Console.WriteLine($"Status: {JsonConvert.SerializeObject(status, Formatting.Indented)}"); + +// // Or use our Execute extensions directly to handle everything for you! + +// // Execute a prepared Buy +// var buyResult = await bridge.Execute(myWallet, preparedBuy); +// var buyHashes = buyResult.Select(receipt => receipt.TransactionHash).ToList(); +// Console.WriteLine($"Buy hashes: {JsonConvert.SerializeObject(buyHashes, Formatting.Indented)}"); + +// // Execute a prepared Sell +// var sellResult = await bridge.Execute(myWallet, preparedSell); +// var sellHashes = sellResult.Select(receipt => receipt.TransactionHash).ToList(); +// Console.WriteLine($"Sell hashes: {JsonConvert.SerializeObject(sellHashes, Formatting.Indented)}"); + +// // Execute a prepared Transfer +// var transferResult = await bridge.Execute(myWallet, preparedTransfer); +// var transferHashes = transferResult.Select(receipt => receipt.TransactionHash).ToList(); +// Console.WriteLine($"Transfer hashes: {JsonConvert.SerializeObject(transferHashes, Formatting.Indented)}"); + +// // Onramp - Get a quote for buying crypto with Fiat +// var preparedOnramp = await bridge.Onramp_Prepare( +// onramp: OnrampProvider.Coinbase, +// chainId: 8453, +// tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base +// amount: "10000000", +// receiver: await myWallet.GetAddress() +// ); +// Console.WriteLine($"Onramp link: {preparedOnramp.Link}"); +// Console.WriteLine($"Full onramp quote and steps data: {JsonConvert.SerializeObject(preparedOnramp, Formatting.Indented)}"); + +// while (true) +// { +// var onrampStatus = await bridge.Onramp_Status(id: preparedOnramp.Id); +// Console.WriteLine($"Full Onramp Status: {JsonConvert.SerializeObject(onrampStatus, Formatting.Indented)}"); +// if (onrampStatus.StatusType is StatusType.COMPLETED or StatusType.FAILED) +// { +// break; +// } +// await ThirdwebTask.Delay(5000); +// } + +// if (preparedOnramp.IsSwapRequiredPostOnramp()) +// { +// // Execute additional steps that are required post-onramp to get to your token, manually or via the Execute extension +// var receipts = await bridge.Execute(myWallet, preparedOnramp); +// Console.WriteLine($"Onramp receipts: {JsonConvert.SerializeObject(receipts, Formatting.Indented)}"); +// } +// else +// { +// Console.WriteLine("No additional steps required post-onramp, you can use the tokens directly!"); +// } #endregion @@ -763,68 +761,6 @@ #endregion -#region Buy with Fiat - -// // Supported currencies -// var supportedCurrencies = await ThirdwebPay.GetBuyWithFiatCurrencies(client); -// Console.WriteLine($"Supported currencies: {JsonConvert.SerializeObject(supportedCurrencies, Formatting.Indented)}"); - -// // Get a Buy with Fiat quote -// var fiatQuoteParamsWithProvider = new BuyWithFiatQuoteParams(fromCurrencySymbol: "USD", toAddress: walletAddress, toChainId: "137", toTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, toAmount: "20", preferredProvider: "STRIPE"); -// var fiatQuoteParams = new BuyWithFiatQuoteParams(fromCurrencySymbol: "USD", toAddress: walletAddress, toChainId: "137", toTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, toAmount: "20"); -// var fiatOnrampQuote = await ThirdwebPay.GetBuyWithFiatQuote(client, fiatQuoteParams); -// Console.WriteLine($"Fiat onramp quote: {JsonConvert.SerializeObject(fiatOnrampQuote, Formatting.Indented)}"); - -// // Get a Buy with Fiat link -// var onRampLink = ThirdwebPay.BuyWithFiat(fiatOnrampQuote); -// Console.WriteLine($"Fiat onramp link: {onRampLink}"); - -// // Open onramp link to start the process (use your framework's version of this) -// var psi = new ProcessStartInfo { FileName = onRampLink, UseShellExecute = true }; -// _ = Process.Start(psi); - -// // Poll for status -// var currentOnRampStatus = OnRampStatus.NONE; -// while (currentOnRampStatus is not OnRampStatus.ON_RAMP_TRANSFER_COMPLETED and not OnRampStatus.ON_RAMP_TRANSFER_FAILED) -// { -// var onRampStatus = await ThirdwebPay.GetBuyWithFiatStatus(client, fiatOnrampQuote.IntentId); -// currentOnRampStatus = Enum.Parse(onRampStatus.Status); -// Console.WriteLine($"Fiat onramp status: {JsonConvert.SerializeObject(onRampStatus, Formatting.Indented)}"); -// await Task.Delay(5000); -// } - -#endregion - -#region Buy with Crypto - -// // Swap Polygon MATIC to Base ETH -// var swapQuoteParams = new BuyWithCryptoQuoteParams( -// fromAddress: walletAddress, -// fromChainId: 137, -// fromTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, -// toTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, -// toChainId: 8453, -// toAmount: "0.1" -// ); -// var swapQuote = await ThirdwebPay.GetBuyWithCryptoQuote(client, swapQuoteParams); -// Console.WriteLine($"Swap quote: {JsonConvert.SerializeObject(swapQuote, Formatting.Indented)}"); - -// // Initiate swap -// var txHash3 = await ThirdwebPay.BuyWithCrypto(wallet: privateKeyWallet, buyWithCryptoQuote: swapQuote); -// Console.WriteLine($"Swap transaction hash: {txHash3}"); - -// // Poll for status -// var currentSwapStatus = SwapStatus.NONE; -// while (currentSwapStatus is not SwapStatus.COMPLETED and not SwapStatus.FAILED) -// { -// var swapStatus = await ThirdwebPay.GetBuyWithCryptoStatus(client, txHash3); -// currentSwapStatus = Enum.Parse(swapStatus.Status); -// Console.WriteLine($"Swap status: {JsonConvert.SerializeObject(swapStatus, Formatting.Indented)}"); -// await Task.Delay(5000); -// } - -#endregion - #region Storage Actions // // Will download from IPFS or normal urls diff --git a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs index 331a6f7..733cbc6 100644 --- a/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs +++ b/Thirdweb/Thirdweb.Bridge/ThirdwebBridge.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Text; using Newtonsoft.Json; namespace Thirdweb.Bridge; @@ -139,25 +140,30 @@ public async Task Buy_Prepare( throw new ArgumentException("receiver is not a valid address", nameof(receiver)); } - var url = $"{Constants.BRIDGE_API_URL}/v1/buy/prepare"; - var queryParams = new Dictionary + var requestBody = new { - { "originChainId", originChainId.ToString() }, - { "originTokenAddress", originTokenAddress }, - { "destinationChainId", destinationChainId.ToString() }, - { "destinationTokenAddress", destinationTokenAddress }, - { "buyAmountWei", buyAmountWei.ToString() }, - { "sender", sender }, - { "receiver", receiver }, - { "maxSteps", maxSteps.ToString() }, - { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null } + originChainId = originChainId.ToString(), + originTokenAddress, + destinationChainId = destinationChainId.ToString(), + destinationTokenAddress, + buyAmountWei = buyAmountWei.ToString(), + sender, + receiver, + maxSteps, + purchaseData }; - url = AppendQueryParams(url, queryParams); - var response = await this._httpClient.GetAsync(url).ConfigureAwait(false); + var url = $"{Constants.BRIDGE_API_URL}/v1/buy/prepare"; + + var jsonBody = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var response = await this._httpClient.PostAsync(url, content).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonConvert.DeserializeObject>(responseContent); + return result.Data; } @@ -280,25 +286,30 @@ public async Task Sell_Prepare( throw new ArgumentException("receiver is not a valid address", nameof(receiver)); } - var url = $"{Constants.BRIDGE_API_URL}/v1/sell/prepare"; - var queryParams = new Dictionary + var requestBody = new { - { "originChainId", originChainId.ToString() }, - { "originTokenAddress", originTokenAddress }, - { "destinationChainId", destinationChainId.ToString() }, - { "destinationTokenAddress", destinationTokenAddress }, - { "sellAmountWei", sellAmountWei.ToString() }, - { "sender", sender }, - { "receiver", receiver }, - { "maxSteps", maxSteps.ToString() }, - { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null } + originChainId = originChainId.ToString(), + originTokenAddress, + destinationChainId = destinationChainId.ToString(), + destinationTokenAddress, + sellAmountWei = sellAmountWei.ToString(), + sender, + receiver, + maxSteps, + purchaseData }; - url = AppendQueryParams(url, queryParams); - var response = await this._httpClient.GetAsync(url).ConfigureAwait(false); + var url = $"{Constants.BRIDGE_API_URL}/v1/sell/prepare"; + + var jsonBody = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var response = await this._httpClient.PostAsync(url, content).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonConvert.DeserializeObject>(responseContent); + return result.Data; } @@ -353,23 +364,28 @@ public async Task Transfer_Prepare( throw new ArgumentException("receiver is not a valid address", nameof(receiver)); } - var url = $"{Constants.BRIDGE_API_URL}/v1/transfer/prepare"; - var queryParams = new Dictionary + var requestBody = new { - { "chainId", chainId.ToString() }, - { "tokenAddress", tokenAddress }, - { "transferAmountWei", transferAmountWei.ToString() }, - { "sender", sender }, - { "receiver", receiver }, - { "feePayer", feePayer }, - { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null } + chainId = chainId.ToString(), + tokenAddress, + transferAmountWei = transferAmountWei.ToString(), + sender, + receiver, + feePayer, + purchaseData }; - url = AppendQueryParams(url, queryParams); - var response = await this._httpClient.GetAsync(url).ConfigureAwait(false); + var url = $"{Constants.BRIDGE_API_URL}/v1/transfer/prepare"; + + var jsonBody = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var response = await this._httpClient.PostAsync(url, content).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonConvert.DeserializeObject>(responseContent); + return result.Data; } @@ -411,33 +427,32 @@ public async Task Onramp_Prepare( throw new ArgumentException("receiver is not a valid address", nameof(receiver)); } - var url = $"{Constants.BRIDGE_API_URL}/v1/onramp/prepare"; - var queryParams = new Dictionary + var requestBody = new { - { "onramp", onramp.ToString().ToLower() }, - { "chainId", chainId.ToString() }, - { "tokenAddress", tokenAddress }, - { "amount", amount }, - { "receiver", receiver }, - { "purchaseData", purchaseData != null ? JsonConvert.SerializeObject(purchaseData) : null }, - { "onrampTokenAddress", onrampTokenAddress }, - { "onrampChainId", onrampChainId?.ToString() }, - { "currency", currency }, - { "maxSteps", maxSteps?.ToString() } + onramp = onramp.ToString().ToLower(), + chainId = chainId.ToString(), + tokenAddress, + amount, + receiver, + onrampTokenAddress, + onrampChainId = onrampChainId?.ToString(), + currency, + maxSteps, + excludeChainIds = excludeChainIds != null && excludeChainIds.Count > 0 ? excludeChainIds.Select(id => id.ToString()).ToList() : null, + purchaseData }; - if (excludeChainIds != null && excludeChainIds.Count > 0) - { - queryParams.Add("excludeChainIds", string.Join(",", excludeChainIds)); - } + var url = $"{Constants.BRIDGE_API_URL}/v1/onramp/prepare"; - url = AppendQueryParams(url, queryParams); + var jsonBody = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - var response = await this._httpClient.GetAsync(url).ConfigureAwait(false); + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var response = await this._httpClient.PostAsync(url, content).ConfigureAwait(false); _ = response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonConvert.DeserializeObject>(responseContent); + return result.Data; }