diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 50ff598..cf240f5 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -54,7 +54,7 @@ // originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum // destinationChainId: 324, // destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync -// buyAmountWei: BigInteger.Parse("0.1".ToWei()) +// buyAmountWei: BigInteger.Parse("0.01".ToWei()) // ); // Console.WriteLine($"Buy quote: {JsonConvert.SerializeObject(buyQuote, Formatting.Indented)}"); @@ -64,11 +64,11 @@ // originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum // destinationChainId: 324, // destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync -// buyAmountWei: BigInteger.Parse("0.1".ToWei()), +// buyAmountWei: BigInteger.Parse("0.01".ToWei()), // sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), // receiver: await myWallet.GetAddress() // ); -// Console.WriteLine($"Prepared Buy contains {preparedBuy.Transactions.Count} transaction(s)!"); +// 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( @@ -76,7 +76,7 @@ // originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync // destinationChainId: 1, // destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum -// sellAmountWei: BigInteger.Parse("0.1".ToWei()) +// sellAmountWei: BigInteger.Parse("0.01".ToWei()) // ); // Console.WriteLine($"Sell quote: {JsonConvert.SerializeObject(sellQuote, Formatting.Indented)}"); @@ -86,17 +86,17 @@ // originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync // destinationChainId: 1, // destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum -// sellAmountWei: BigInteger.Parse("0.1".ToWei()), +// sellAmountWei: BigInteger.Parse("0.01".ToWei()), // sender: await Utils.GetAddressFromENS(client, "vitalik.eth"), // receiver: await myWallet.GetAddress() // ); -// Console.WriteLine($"Prepared Sell contains {preparedSell.Transactions.Count} transaction(s)!"); +// 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, // ETH on zkSync -// transferAmountWei: BigInteger.Parse("0.1".ToWei()), +// 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() // ); @@ -128,6 +128,39 @@ // 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 #region Indexer @@ -728,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.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..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; @@ -32,9 +33,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 +72,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 +94,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 +105,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) @@ -126,23 +140,30 @@ string receiver throw new ArgumentException("receiver is not a valid address", nameof(receiver)); } - var url = $"{Constants.BRIDGE_API_URL}/v1/buy/prepare"; - var queryParams = new Dictionary - { - { "originChainId", originChainId.ToString() }, - { "originTokenAddress", originTokenAddress }, - { "destinationChainId", destinationChainId.ToString() }, - { "destinationTokenAddress", destinationTokenAddress }, - { "buyAmountWei", buyAmountWei.ToString() }, - { "sender", sender }, - { "receiver", receiver } + var requestBody = new + { + 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; } @@ -158,9 +179,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 +218,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 +240,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 +251,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) @@ -252,23 +286,30 @@ string receiver throw new ArgumentException("receiver is not a valid address", nameof(receiver)); } - var url = $"{Constants.BRIDGE_API_URL}/v1/sell/prepare"; - var queryParams = new Dictionary - { - { "originChainId", originChainId.ToString() }, - { "originTokenAddress", originTokenAddress }, - { "destinationChainId", destinationChainId.ToString() }, - { "destinationTokenAddress", destinationTokenAddress }, - { "sellAmountWei", sellAmountWei.ToString() }, - { "sender", sender }, - { "receiver", receiver } + var requestBody = new + { + 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; } @@ -284,9 +325,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) { @@ -313,21 +364,113 @@ public async Task Transfer_Prepare(BigInteger chainId, stri throw new ArgumentException("receiver is not a valid address", nameof(receiver)); } + var requestBody = new + { + chainId = chainId.ToString(), + tokenAddress, + transferAmountWei = transferAmountWei.ToString(), + sender, + receiver, + feePayer, + purchaseData + }; + var url = $"{Constants.BRIDGE_API_URL}/v1/transfer/prepare"; - var queryParams = new Dictionary + + 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; + } + + #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)) { - { "chainId", chainId.ToString() }, - { "tokenAddress", tokenAddress }, - { "transferAmountWei", transferAmountWei.ToString() }, - { "sender", sender }, - { "receiver", receiver } + throw new ArgumentException("receiver is not a valid address", nameof(receiver)); + } + + var requestBody = new + { + 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 }; + + var url = $"{Constants.BRIDGE_API_URL}/v1/onramp/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; + } + + 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); + var result = JsonConvert.DeserializeObject>(responseContent); return result.Data; } @@ -372,6 +515,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}"); }