From 8acb41f3df37dfd9eb073467ea34d6feac01812d Mon Sep 17 00:00:00 2001 From: Ilnur Date: Thu, 13 Mar 2025 00:46:14 +0400 Subject: [PATCH 01/15] add fireblocks key type fo transfer function --- cmd/keycmd/fireblocks/fireblocks_domain.go | 537 ++++++++++++ cmd/keycmd/fireblocks/fireblocks_sdk.go | 963 +++++++++++++++++++++ cmd/keycmd/fireblocks/keychain.go | 78 ++ cmd/keycmd/transfer.go | 30 +- go.mod | 10 +- go.sum | 19 + pkg/utils/keys.go | 1 + 7 files changed, 1633 insertions(+), 5 deletions(-) create mode 100644 cmd/keycmd/fireblocks/fireblocks_domain.go create mode 100644 cmd/keycmd/fireblocks/fireblocks_sdk.go create mode 100644 cmd/keycmd/fireblocks/keychain.go diff --git a/cmd/keycmd/fireblocks/fireblocks_domain.go b/cmd/keycmd/fireblocks/fireblocks_domain.go new file mode 100644 index 000000000..25cd46c8e --- /dev/null +++ b/cmd/keycmd/fireblocks/fireblocks_domain.go @@ -0,0 +1,537 @@ +package fireblocks + +import ( + "github.com/shopspring/decimal" +) + +type CreateTransactionPayload struct { + AssetId string `json:"assetId"` + Source TransferPeerPath `json:"source"` // source of the transaction + Destination DestinationTransferPeerPath `json:"destination"` // Destination of the transaction + Destinations []DestinationTransferPeerPath `json:"destinations,omitempty"` // Destination of the transaction + Amount string `json:"amount"` // If the transfer is a withdrawal from an exchange, the actual amount that was requested to be transferred. Otherwise, the requested amount + Fee string `json:"fee,omitempty"` // The total fee deducted by the exchange from the actual requested amount (serviceFee = amount - netAmount) + TreatAsGrossAmount bool `json:"treatAsGrossAmount,omitempty"` // For outgoing transactions, if true, the network fee is deducted from the requested amount + FailOnLowFee bool `json:"failOnLowFee,omitempty"` + NetworkFee string `json:"networkFee,omitempty"` //The fee paid to the network + PriorityFee string `json:"priorityFee,omitempty"` + FeeLevel string `json:"feeLevel,omitempty"` + MaxFee string `json:"maxFee,omitempty"` + Note string `json:"note,omitempty"` // Customer note of the transaction + Operation string `json:"operation"` // Default operation is "TRANSFER" + CustomerRefId string `json:"customerRefId,omitempty"` // The ID for AML providers to associate the owner of funds with transactions + ReplacedTxHash string `json:"replacedTxHash,omitempty"` // + ExtraParameters ExtraParameters `json:"extraParameters,omitempty"` // Protocol / operation specific parameters. + ExternalTxId string `json:"externalTxId,omitempty"` + WaitForStatus bool `json:"waitForStatus,omitempty"` + GasPrice string `json:"gasPrice,omitempty"` + GasLimit string `json:"gasLimit,omitempty"` + CpuStaking string `json:"cpuStaking,omitempty"` + NetworkStaking string `json:"networkStaking,omitempty"` + AutoStaking string `json:"autoStaking,omitempty"` +} + +type ExternalWalletAsset struct { + Id string `json:"id"` // the id of the asset + Status ConfigChangeRequestStatus `json:"status"` // Status of the External Wallet + ActivationTime string `json:"activationTime"` // The time the wallet will be activated in case wallets activation posponed according to workspace definition + Address string `json:"address"` // The address of the wallet + Tag string `json:"tag"` // Destination tag (for XRP, used as memo for EOS/XLM) of the wallet, for SEN/Signet used as Bank Transfer Description + +} + +type ExternalWallet struct { + Id string `json:"id"` //The ID of the Unmanaged Wallet + Name string `json:"name"` // Name of the Wallet Container + CustomerRefId string `json:"customerRefId,omitempty"` //[optional] The ID for AML providers to associate the owner of funds with transactions + Assets []ExternalWalletAsset `json:"assets"` //Array of the assets available in the exteral wallet +} + +type UnmanagedWallet struct { + Id string + Name string + CustomerRefId string + Assets []WalletAsset +} + +type WalletAsset struct { + Id string `json:"id"` // the id of the asset + Balance string `json:"balance"` // the balance of the wallet + LockedAmount string `json:"lockedAmount"` // locked amount in the wallet + Status ConfigChangeRequestStatus `json:"status"` // Status of the External Wallet + ActivationTime string `json:"activationTime"` // The time the wallet will be activated in case wallets activation posponed according to workspace definition + Address string `json:"address"` // The address of the wallet + Tag string `json:"tag"` // Destination tag (for XRP, used as memo for EOS/XLM) of the wallet, for SEN/Signet used as Bank Transfer Description + +} + +type User struct { + Id string `json:"id"` // User ID on the Fireblocks platform + FirstName string `json:"firstName"` // First name + LastName string `json:"lastName"` // Last name + Role string `json:"role"` // The role of the user in the workspace + Email string `json:"email"` // The email of the user + Enabled bool `json:"enabled"` //The status of the user in the workspace +} + +type ConfigChangeRequestStatus string + +const ( + WaitingForApproval ConfigChangeRequestStatus = "WAITING_FOR_APPROVAL" + Approved = "APPROVED" + Cancelled = "CANCELLED" + Rejected = "REJECTED" + Failed = "FAILED" +) + +type CreateAddressResponse struct { + Address string `json:"address"` //Address of the asset in a Vault Account, for BTC/LTC the address is in Segwit (Bech32) format, cash address format for BCH + LegacyAddress string `json:"legacyAddress"` // Legacy address format for BTC/LTC/BCH + Tag string `json:"tag"` // Destination tag for XRP, used as memo for EOS/XLM +} + +type ExtraParameters struct { + ContractCallData string `json:"contractCallData"` +} + +type ErrorMessage struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type TransactionStatus string + +const ( + TransactionStatusSubmitted TransactionStatus = "SUBMITTED" + TransactionStatusQueued = "QUEUED" + TransactionPendingSignature = "PENDING_SIGNATURE" + TransactionPendingAuthorization = "PENDING_AUTHORIZATION" + TransactionPending3rdPartyManualApproval = "PENDING_3RD_PARTY_MANUAL_APPROVAL" + TransactionPending3rdParty = "PENDING_3RD_PARTY" + TransactionPending = "PENDING" // Deprecated + TransactionBroadcasting = "BROADCASTING" + TransactionConfirming = "CONFIRMING" + TransactionConfirmed = "CONFIRMED" // Deprecated + TransactionCompleted = "COMPLETED" + TransactionPendingAmlCheckup = "PENDING_AML_CHECKUP" + TransactionPartiallyCompleted = "PARTIALLY_COMPLETED" + TransactionCancelling = "CANCELLING" + TransactionCancelled = "CANCELLED" + TransactionRejected = "REJECTED" + TransactionFailed = "FAILED" + TransactionTimeout = "TIMEOUT" + TransactionBlocked = "BLOCKED" +) + +type TransactionSubStatus string + +const ( + InsufficientFunds TransactionSubStatus = "INSUFFICIENT_FUNDS" + AmountTooSmall = "AMOUNT_TOO_SMALL" + UnsupportedAsset = "UNSUPPORTED_ASSET" + UnauthorisedMissingPermission = "UNAUTHORISED__MISSING_PERMISSION" + InvalidSignature = "INVALID_SIGNATURE" + ApiInvalidSignature = "API_INVALID_SIGNATURE" + UnauthorisedMissingCredentials = "UNAUTHORISED__MISSING_CREDENTIALS" + UnauthorisedUser = "UNAUTHORISED__USER" + UnauthorisedDevice = "UNAUTHORISED__DEVICE" + InvalidUnmanagedWallet = "INVALID_UNMANAGED_WALLET" + InvalidExchangeAccount = "INVALID_EXCHANGE_ACCOUNT" + InsufficientFundsForFee = "INSUFFICIENT_FUNDS_FOR_FEE" + InvalidAddress = "INVALID_ADDRESS" + WithdrawLimit = "WITHDRAW_LIMIT" + ApiCallLimit = "API_CALL_LIMIT" + AddressNotWhitelisted = "ADDRESS_NOT_WHITELISTED" + TIMEOUT = "TIMEOUT" + ConnectivityError = "CONNECTIVITY_ERROR" + ThirdPartyInternalError = "THIRD_PARTY_INTERNAL_ERROR" + CancelledExternally = "CANCELLED_EXTERNALLY" + InvalidThirdPartyResponse = "INVALID_THIRD_PARTY_RESPONSE" + VaultWalletNotReady = "VAULT_WALLET_NOT_READY" + MissingDepositAddress = "MISSING_DEPOSIT_ADDRESS" + OneTimeAddressDisabled = "ONE_TIME_ADDRESS_DISABLED" + InternalError = "INTERNAL_ERROR" + UnknownError = "UNKNOWN_ERROR" + AuthorizerNotFound = "AUTHORIZER_NOT_FOUND" + InsufficientReservedFunding = "INSUFFICIENT_RESERVED_FUNDING" + ManualDepositAddressRequired = "MANUAL_DEPOSIT_ADDRESS_REQUIRED" + InvalidFee = "INVALID_FEE" + ErrorUnsupportedTransactionType = "ERROR_UNSUPPORTED_TRANSACTION_TYPE" + UnsupportedOperation = "UNSUPPORTED_OPERATION" + T3rdPartyProcessing = "3RD_PARTY_PROCESSING" + PendingBlockchainConfirmations = "PENDING_BLOCKCHAIN_CONFIRMATIONS" + T3rdPartyConfirming = "3RD_PARTY_CONFIRMING" + CONFIRMED = "CONFIRMED" + T3rdPartyCompleted = "3RD_PARTY_COMPLETED" + RejectedByUser = "REJECTED_BY_USER" + CancelledByUser = "CANCELLED_BY_USER" + T3rdPartyCancelled = "3RD_PARTY_CANCELLED" + T3rdPartyRejected = "3RD_PARTY_REJECTED" + AmlScreeningRejected = "AML_SCREENING_REJECTED" + BlockedByPolicy = "BLOCKED_BY_POLICY" + FailedAmlScreening = "FAILED_AML_SCREENING" + PartiallyFailed = "PARTIALLY_FAILED" + T3rdPartyFailed = "3RD_PARTY_FAILED" + DroppedByBlockchain = "DROPPED_BY_BLOCKCHAIN" + TooManyInputs = "TOO_MANY_INPUTS" + SigningError = "SIGNING_ERROR" + InvalidFeeParams = "INVALID_FEE_PARAMS" + MissingTagOrMemo = "MISSING_TAG_OR_MEMO" + GasLimitTooLow = "GAS_LIMIT_TOO_LOW" + MaxFeeExceeded = "MAX_FEE_EXCEEDED" + ActualFeeTooHigh = "ACTUAL_FEE_TOO_HIGH" + InvalidContractCallData = "INVALID_CONTRACT_CALL_DATA" + InvalidNonceTooLow = "INVALID_NONCE_TOO_LOW" + InvalidNonceTooHigh = "INVALID_NONCE_TOO_HIGH" + InvalidNonceForRbf = "INVALID_NONCE_FOR_RBF" + FailOnLowFee = "FAIL_ON_LOW_FEE" + TooLongMempoolChain = "TOO_LONG_MEMPOOL_CHAIN" + TxOutdated = "TX_OUTDATED" + IncompleteUserSetup = "INCOMPLETE_USER_SETUP" + SignerNotFound = "SIGNER_NOT_FOUND" + InvalidTagOrMemo = "INVALID_TAG_OR_MEMO" + ZeroBalanceInPermanentAddress = "ZERO_BALANCE_IN_PERMANENT_ADDRESS" + NeedMoreToCreateDestination = "NEED_MORE_TO_CREATE_DESTINATION" + NonExistingAccountName = "NON_EXISTING_ACCOUNT_NAME" + EnvUnsupportedAsset = "ENV_UNSUPPORTED_ASSET" +) + +type TransactionDetails struct { + Id string `json:"id"` // ID of the transaction + AssetId string `json:"AssetId"` + Source TransferPeerPathResponse `json:"source"` // source of the transaction + Destination TransferPeerPathResponse `json:"destination"` // Destination of the transaction + RequestedAmount decimal.Decimal `json:"RequestedAmount"` // the amount requested by the user + AmountInfo AmountInfo `json:"amountInfo"` // Details of the transaction's amount in string format + FeeInfo FeeInfo `json:"feeInfo"` // Details of the transaction's fee in string format + Amount decimal.Decimal `json:"amount"` // If the transfer is a withdrawal from an exchange, the actual amount that was requested to be transferred. Otherwise, the requested amount + NetAmount decimal.Decimal `json:"netAmount"` // The net amount of the transaction, after fee deduction + AmountUSD decimal.Decimal `json:"amountUSD"` // The USD value of the requested amount + ServiceFee decimal.Decimal `json:"ServiceFee"` // The total fee deducted by the exchange from the actual requested amount (serviceFee = amount - netAmount) + TreatAsGrossAmount bool `json:"treatAsGrossAmount"` // For outgoing transactions, if true, the network fee is deducted from the requested amount + NetworkFee decimal.Decimal `json:"networkFee"` //The fee paid to the network + CreatedAt int64 `json:"createdAt"` // Unix timestamp + LastUpdated int64 `json:"lastUpdated"` // Unix timestamp + Status TransactionStatus `json:"status"` // The current status of the transaction + TxHash string `json:"txHash"` // Blockchain hash of the transaction + SubStatus TransactionSubStatus `json:"subStatus"` // More detailed status of the transaction + SourceAddress string `json:"sourceAddress"` // For account based assets only, the source address of the transaction + DestinationAddress string `json:"destinationAddress"` // Address where the asset were transfered + DestinationAddressDescription string `json:"destinationAddressDescription"` // Description of the address + DestinationTag string `json:"destinationTag"` // Destination tag (for XRP, used as memo for EOS/XLM) or Bank Transfer Description for Signet/SEN + SignedBy []string `json:"signedBy"` //Signers of the transaction + CreatedBy string `json:"createdBy"` // Initiator of the transaction + RejectedBy string `json:"rejectedBy"` // User ID of the user that rejected the transaction (in case it was rejected) + AddressType string `json:"addressType"` // [ ONE_TIME, WHITELISTED ] + Note string `json:"note"` // Customer note of the transaction + ExchangeTxId string `json:"exchangeTxId"` // If the transaction originated from an exchange, this is the exchange tx ID + FeeCurrency string `json:"feeCurrency"` // The asset which was taken to pay the fee (ETH for ERC-20 tokens, BTC for Tether Omni) + Operation string `json:"operation"` // Default operation is "TRANSFER" + AmlScreeningResult AmlScreeningResult `json:"amlScreeningResult"` // The result of the AML screening + CustomerRefId string `json:"customerRefId"` // The ID for AML providers to associate the owner of funds with transactions + NumberOfConfirmations int `json:"numberOfConfirmations"` // The number of confirmations of the transaction. The number will increase until the transaction will be considered completed according to the confirmation policy. + NetworkRecords []NetworkRecord `json:"networkRecords"` // Transaction on the Fireblocks platform can aggregate several blockchain transactions, in such a case these records specify all the transactions that took place on the blockchain. + ReplacedTxHash string `json:"replacedTxHash"` // In case of an RBF transaction, the hash of the dropped transaction + ExternalTxId string `json:"externalTxId"` // Unique transaction ID provided by the user + Destinations []DestinationsResponse `json:"destinations"` // For UTXO based assets, all outputs specified here + BlockInfo BlockInfo `json:"blockInfo"` //The information of the block that this transaction was mined in, the blocks's hash and height + SignedMessages []SignedMessage `json:"signedMessages"` // A list of signed messages returned for raw signing + ExtraParameters map[string]interface{} `json:"extraParameters"` // Protocol / operation specific parameters. + +} + +type BlockInfo struct { + BlockHeight string `json:"blockHeight"` + BlockHash string `json:"blockHash"` +} + +type TransactionFee struct { + FeePerByte string `json:"feePerByte"` // [optional] For UTXOs, + GasPrice string `json:"gasPrice"` // [optional] For Ethereum assets (ETH and Tokens) + GasLimit string ` json:"gasLimit"` // [optional] For Ethereum assets (ETH and Tokens), the limit for how much can be used + NetworkFee string `json:"networkFee"` // [optional] Transaction fee +} + +type EstimatedTransactionFeeResponse struct { + Low TransactionFee `json:"low"` //Transactions with this fee will probably take longer to be mined + Medium TransactionFee `json:"medium"` // Average transactions fee + High TransactionFee `json:"high"` //Transactions with this fee should be mined the fastest + +} + +type AddressStatus struct { + IsValid bool `json:"isValid"` + IsActive bool `json:"isActive"` + RequiresTag bool `json:"requiresTag"` +} + +type SignedMessage struct { + Content string `json:"content"` // The message for signing (hex-formatted) + Algorithm string `json:"algorithm"` // The algorithm that was used for signing, one of the SigningAlgorithms + DerivationPath string `json:"derivationPath"` // BIP32 derivation path of the signing key. E.g. [44,0,46,0,0] + Signature map[string]interface{} `json:"signature"` // The message signature + PublicKey string `json:"publicKey"` // Signature's public key that can be used for verification. +} + +type DestinationsResponse struct { + Amount decimal.Decimal `json:"amount"` // The amount to be sent to this destination + Destination TransferPeerPathResponse `json:"destination"` // Destination of the transaction + AmountUSD decimal.Decimal `json:"amountUSD"` // The USD value of the requested amount + DestinationAddress string `json:"destinationAddress"` // Address where the asset were transfered + DestinationAddressDescription string `json:"destinationAddressDescription"` // Description of the address + AmlScreeningResult AmlScreeningResult `json:"amlScreeningResult"` // The result of the AML screening + CustomerRefId string `json:"customerRefId"` // The ID for AML providers to associate the owner of funds with transactions + +} + +type NetworkRecord struct { + Source TransferPeerPathResponse `json:"source"` // Source of the transaction + Destination TransferPeerPathResponse `json:"destination"` // Destination of the transaction + TxHash string `json:"txHash"` // Blockchain hash of the transaction + NetworkFee decimal.Decimal `json:"networkFee"` // The fee paid to the network + AssetId string `json:"assetId"` // transaction asset + NetAmount decimal.Decimal `json:"netAmount"` // The net amount of the transaction, after fee deduction + Status NetworkStatus `json:"status"` // Status of the blockchain transaction + OpType string `json:"type"` // Type of the operation + DestinationAddress string `json:"destinationAddress"` // Destination address + SourceAddress string `json:"sourceAddress"` // For account based assets only, the source address of the transaction + +} + +type NetworkStatus string + +const ( + DROPPED NetworkStatus = "DROPPED" + BROADCASTING = "BROADCASTING" + CONFIRMING = "CONFIRMING" + FAILED = "FAILED" + NsConfirmed = "CONFIRMED" +) + +type AmlScreeningResult struct { + Provider string `json:"provider"` // The AML service provider + Payload string `json:"payload"` // The response of the AML service provider +} + +type AmountInfo struct { + Amount string `json:"amount"` // If the transfer is a withdrawal from an exchange, the actual amount that was requested to be transferred. Otherwise, the requested amount + RequestedAmount string `json:"requestedAmount"` //The amount requested by the user + NetAmount string `json:"NetAmount"` // The net amount of the transaction, after fee deduction + AmountUSD string `json:"amountUSD"` // The USD value of the requested amount +} + +type FeeInfo struct { + NetworkFee string `json:"NetworkFee"` // The fee paid to the network + ServiceFee string `json:"ServiceFee"` // The total fee deducted by the exchange from the actual requested amount (serviceFee = amount - netAmount) +} + +type CreateTransactionResponse struct { + Id string `json:"Id"` + Status TransactionStatus `json:"status"` + Error error +} + +type CreateVaultAssetResponse struct { + Id string `json:"Id"` // the Id of the asset + Address string `json:"address"` // Address of the asset in a Vault Account, for BTC/LTC the address is in segwit (Bech32) format, cash address format BCH + LegacyAddress string `json:"legacyAddress"` // legacy address format for BTC/LTC/BCH + Tag string `json:"tag"` // destination tag for XRP, memo for EOS/XLM + EosAccountName string `json:"eosAccountName"` // returned for EOS, the acct name. +} + +type AssetTypeResponse struct { + Id string `json:"Id"` + Name string `json:"name"` + AssetType string `json:"type"` + ContractAddress string `json:"contractAddress"` + NativeAsset string `json:"nativeAsset"` +} + +type VaultAccountAssetAddress struct { + AssetId string `json:"assetId"` // The ID of the asset + Address string `json:"address"` // Address of the asset in a Vault Account, for BTC/LTC the address is in Segwit (Bech32) format, for BCH cash format + LegacyAddress string `json:"legacyAddress"` // For BTC/LTC/BCH the legacy format address + Description string `json:"description"` // Description of the address + Tag string `json:"tag"` // Destination tag for XRP, used as memo for EOS/XLM, for Signet/SEN it is the Bank Transfer Description + Type string `json:"type"` // Address type + CustomerRefId string `json:" customerRefId"` // [optional] The ID for AML providers to associate the owner of funds with transactions + AddressFormat string `json:"addressFormat"` + EnterpriseAddress string `json:"enterpriseAddress"` +} + +type UnsignedMessage struct { + Content string `json:"content"` //message to be signed - hex format. + Bip44AddressIndex int `json:"bip44AddressIndex"` // + Bib44Change int `json:"bib44Change"` //bit44 change path level + DerivationPath []int `json:"derivationPath"` +} + +type RawMessage struct { + messages []UnsignedMessage + algorithm SigningAlgorithm +} + +type OneTimeAddress struct { + Address string `json:"address"` + Tag string `json:"tag"` +} + +type TransferPeerPathResponse struct { + TransferType string `json:"type"` //[ PTVaultAccount, EXCHANGE_ACCOUNT, INTERNAL_WALLET, EXTERNAL_WALLET, ONE_TIME_ADDRESS, NETWORK_CONNECTION, FIAT_ACCOUNT, COMPOUND ] + Id string `json:"id"` // The ID of the exchange account to return + Name string `json:"name"` // The name of the exchange account + Subtype string `json:"subType"` +} + +type TransferPeerPath struct { + TPeerId string `json:"id"` + TPeerType PeerType `json:"type"` +} + +type DestinationTransferPeerPath struct { + TPeerId string `json:"id"` + TPeerType PeerType `json:"type"` + Ota OneTimeAddress `json:"oneTimeAddress"` +} + +type VaultAccount struct { + Id string `json:"id"` + Name string `json:"name"` + HiddenOnUI bool `json:"hiddenOnUI"` + CustomerRefId string `json:"customerRefId"` + AutoFuel bool `json:"autoFuel"` + Assets []VaultAsset `json:"assets"` +} + +type VaultAsset struct { + Id string `json:"id"` + Total string `json:"total"` + Available string `json:"available"` + Pending string `json:"pending"` + LockedAmount string `json:"lockedAmount"` + TotalStackedCPU string `json:"totalStackedCPU"` + TotalStackedNetwork string `json:"totalStackedNetwork"` + SelfStackedCPU string `json:"selfStackedCPU"` + SelfStakedNetwork string `json:"selfStakedNetwork"` + PendingRefundCPU string `json:"pendingRefundCPU"` + PendingRefundNetwork string `json:"pendingRefundNetwork"` +} + +type ExchangeAccount struct { + Id string `json:"id"` + Type ExchangeType `json:"type"` + Name string `json:"name"` + Status ConfigChangeRequestStatus `json:"status"` + Assets []ExchangeAsset `json:"assets"` + IsSubAccount bool `json:"isSubaccount"` + MainAccountId string `json:"mainAccountId"` +} + +type TradingAccount struct { + Type TradingAccountType `json:"type"` + Assets []ExchangeAsset `json:"assets"` +} + +type ExchangeAsset struct { + Id string `json:"id"` + Total string `json:"total"` + Available string `json:"available"` + LockedAmount string `json:"lockedAmount"` + Balance string `json:"balance"` +} + +type ExchangeType struct { + Type string `json:"type"` +} + +type TradingAccountType ExchangeType + +type AssetAddedData struct { + AccountId string `json:"accountId"` // The ID of the vault account under which the wallet was added + TenantId string `json:"tenantId"` // Unique id of your Fireblocks' workspace + AccountName string `json:"accountName"` // The name of the vault account under which the wallet was added + AssetId string `json:"assetId"` // Wallet's asset +} + +type WalletAssetWebhook struct { + AssetId string `json:"assetId"` // Wallet's asset + Id string `json:"id"` // The ID of the wallet + Name string `json:"name"` // The name of wallet + Address string `json:"address"` // The address of the wallet + Tag string `json:"tag"` //Destination tag (for XRP, used as memo for EOS/XLM and as Bank Transfer Description for Signet/SEN) of the wallet + ActivationTime string `json:"activationTime"` // The time the wallet will be activated in case wallets activation posponed according to workspace definition + +} + +type ThirdPartyWebhook struct { + Id string `json:"id"` // Id of the thirdparty account on the Fireblocks platform + SubType string `json:"subType"` // Subtype of the third party, ie. exchange or fiat name + Name string `json:"name"` // Account name +} + +type ObjectAdded struct { + Type string `json:"type"` + TenantId string `json:"tenantId"` + Timestamp int64 `json:"timestamp"` + Data interface{} `json:"data"` +} + +type ExternalWalletAssetAdded ObjectAdded +type ExchangeAccountAdded ObjectAdded +type FiatAccountAdded ObjectAdded +type NetworkConnectionAdded ObjectAdded + +type PeerType string + +const ( + PTVaultAccount PeerType = "VAULT_ACCOUNT" + PTExchangeAccount = "EXCHANGE_ACCOUNT" + PTInternalWallet = "INTERNAL_WALLET" + PTExternalWallet = "EXTERNAL_WALLET" + PTUnknownPeer = "UNKNOWN" + PTFiatAccount = "FIAT_ACCOUNT" + PTNetworkConnection = "NETWORK_CONNECTION" + PTCompound = "COMPOUND" +) + +type EventType string + +const ( + EventTransactionCreated EventType = "TRANSACTION_CREATED" + EventTransactionStatusUpdated = "TRANSACTION_STATUS_UPDATED" + EventVaultAccountAdded = "VAULT_ACCOUNT_ADDED" + EventVaultAccountAssetAdded = "VAULT_ACCOUNT_ASSET_ADDED" + EventInternalWalletAssetAdded = "INTERNAL_WALLET_ASSET_ADDED" + EventExternalWalletAssetAdded = "EXTERNAL_WALLET_ASSET_ADDED" + EventExchangeAccountAdded = "EXCHANGE_ACCOUNT_ADDED" + EventFiatAccountAdded = "FIAT_ACCOUNT_ADDED" + EventNetworkConnectionAdded = "NETWORK_CONNECTION_ADDED" +) + +type SigningAlgorithm string + +const ( + MpcEcdsaSecp256k1 SigningAlgorithm = "MPC_ECDSA_SECP256K1" + MpcEddsaEd25519 = "MPC_EDDSA_ED25519" +) + +type FeeLevel string + +const ( + HIGH FeeLevel = "HIGH" + MEDIUM = "MEDIUM" + LOW = "LOW" +) + +type TransactionType string + +const ( + TxTransfer TransactionType = "TRANSFER" + TxMint = "MINT" + TxBurn = "BURN" + TxSupplyToCompound = "SUPPLY_TO_COMPOUND" + TxnRedeemFromCompound = "REDEEM_FROM_COMPOUND" + TxRaw = "RAW" + TxContractCall = "CONTRACT_CALL" + TxOneTimeAddress = "ONE_TIME_ADDRESS" +) diff --git a/cmd/keycmd/fireblocks/fireblocks_sdk.go b/cmd/keycmd/fireblocks/fireblocks_sdk.go new file mode 100644 index 000000000..f6d10a41a --- /dev/null +++ b/cmd/keycmd/fireblocks/fireblocks_sdk.go @@ -0,0 +1,963 @@ +package fireblocks + +import ( + "bytes" + crand "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/gojek/heimdall/v7/hystrix" + "github.com/golang-jwt/jwt" + "github.com/shopspring/decimal" + log "github.com/sirupsen/logrus" + "io" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "strings" + "time" +) + +type FbKeyMgmt struct { + privateKey *rsa.PrivateKey + apiKey string + rnd *rand.Rand +} + +func NewInstanceKeyMgmt(pk *rsa.PrivateKey, apiKey string) *FbKeyMgmt { + var s secrets + k := new(FbKeyMgmt) + k.privateKey = pk + k.apiKey = apiKey + k.rnd = rand.New(s) + return k +} + +const timeout = 5 * time.Millisecond + +type secrets struct{} + +func (s secrets) Seed(seed int64) {} + +func (s secrets) Uint64() (r uint64) { + err := binary.Read(crand.Reader, binary.BigEndian, &r) + if err != nil { + log.Error(err) + } + return r +} + +func (s secrets) Int63() int64 { + return int64(s.Uint64() & ^uint64(1<<63)) +} + +func (k *FbKeyMgmt) createAndSignJWTToken(path string, bodyJSON string) (string, error) { + + token := &jwt.MapClaims{ + "uri": path, + "nonce": k.rnd.Int63(), + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Second * 55).Unix(), + "sub": k.apiKey, + "bodyHash": createHash(bodyJSON), + } + + j := jwt.NewWithClaims(jwt.SigningMethodRS256, token) + signedToken, err := j.SignedString(k.privateKey) + if err != nil { + log.Error(err) + } + + return signedToken, err +} + +func createHash(data string) string { + h := sha256.New() + h.Write([]byte(data)) + hashed := h.Sum(nil) + return hex.EncodeToString(hashed) +} + +type SDK struct { + httpClient *hystrix.Client + apiBaseURL string + kto *FbKeyMgmt +} + +// NewInstance - create new type to handle Fireblocks API requests +func NewInstance(pk []byte, ak string, url string, t time.Duration) *SDK { + + if t == time.Duration(0) { + // use default + t = timeout + } + + s := new(SDK) + s.apiBaseURL = url + privateK, err := jwt.ParseRSAPrivateKeyFromPEM(pk) + if err != nil { + log.Error(err) + } + + s.kto = NewInstanceKeyMgmt(privateK, ak) + s.httpClient = newCircuitBreakerHttpClient(t) + return s +} + +func newCircuitBreakerHttpClient(t time.Duration) *hystrix.Client { + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ + InsecureSkipVerify: false, + } + c := hystrix.NewClient(hystrix.WithHTTPTimeout(t), + hystrix.WithFallbackFunc(func(err error) error { + log.Errorf("no fallback func implemented: %s", err) + return err + })) + return c +} + +// getRequest - internal method to handle API call to Fireblocks +func (s *SDK) getRequest(path string) (string, error) { + + urlEndPoint := s.apiBaseURL + path + token, err := s.kto.createAndSignJWTToken(path, "") + if err != nil { + log.Error(err) + return fmt.Sprintf("{message: \"%s.\"}", "error signing JWT token"), err + } + + request, err := http.NewRequest(http.MethodGet, urlEndPoint, nil) + if err != nil { + log.Error(err) + return fmt.Sprintf("{message: \"%s.\"}", "error creating NewRequest"), err + } + + request.Header.Add("X-API-Key", s.kto.apiKey) + request.Header.Add("Authorization", fmt.Sprintf("Bearer %v", token)) + + response, err := s.httpClient.Do(request) + if err != nil { + log.Error(err) + return "", err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Error(err) + } + }(response.Body) + + data, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Errorf("error communicating with fireblocks: %v", err) + return "", err + } + + if response.StatusCode >= 300 { + errMsg := fmt.Sprintf("fireblocks server: %s \n %s", response.Status, string(data)) + log.Warning(errMsg) + } + + return string(data), err +} + +func (s *SDK) changeRequest(path string, payload []byte, idempotencyKey string, requestType string) (string, error) { + + urlEndPoint := s.apiBaseURL + path + token, err := s.kto.createAndSignJWTToken(path, string(payload)) + if err != nil { + log.Error(err) + return fmt.Sprintf("{message: \"%s.\"}", "error signing JWT token"), err + } + + request, err := http.NewRequest(requestType, urlEndPoint, bytes.NewBuffer(payload)) + if err != nil { + log.Error(err) + return fmt.Sprintf("{message: \"%s.\"}", "error creating NewRequest"), err + } + request.Header.Add("X-API-Key", string(s.kto.apiKey)) + request.Header.Add("Authorization", fmt.Sprintf("Bearer %v", token)) + request.Header.Add("Content-Type", "application/json") + + if len(idempotencyKey) > 0 { + request.Header.Add("Idempotency-Key", idempotencyKey) + } + response, err := s.httpClient.Do(request) + if err != nil { + log.Error(err) + return "", err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Error(err) + } + }(response.Body) + + data, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Errorf("error on communicating with Fireblocks: %v \n data: %s", err, data) + return "", err + } + + if response.StatusCode >= 300 { + errMsg := fmt.Sprintf("fireblocks server: %s \n %s", response.Status, string(data)) + log.Warning(errMsg) + } + + return string(data), err + +} + +func (s *SDK) GetUsers() ([]User, error) { + + returnedData, err := s.getRequest("/v1/users") + if err != nil { + log.Error(err) + return nil, err + } + + var users []User + err = json.Unmarshal([]byte(returnedData), &users) + if err != nil { + log.Error(err) + return nil, err + } + + return users, nil +} + +// GetSupportedAssets - Gets all assets that are currently supported by Fireblocks API. +func (s *SDK) GetSupportedAssets() ([]AssetTypeResponse, error) { + + returnedData, err := s.getRequest("/v1/supported_assets") + if err != nil { + log.Error(err) + return nil, err + } + var assetsTypeResponse []AssetTypeResponse + err = json.Unmarshal([]byte(returnedData), &assetsTypeResponse) + if err != nil { + log.Error(err) + return nil, err + } + + return assetsTypeResponse, nil +} + +// GetVaultAccounts - gets all vault accounts for the tenant. +func (s *SDK) GetVaultAccounts(namePrefix string, nameSuffix string, minAmountThreshold decimal.Decimal) ([]VaultAccount, error) { + + query := "/v1/vault/accounts" + params := url.Values{} + + if namePrefix != "" { + params.Add("namePrefix", namePrefix) + } + if nameSuffix != "" { + params.Add("nameSuffix", nameSuffix) + } + if minAmountThreshold.GreaterThan(decimal.NewFromFloat(0.0)) { + params.Add("minAmountThreshold", fmt.Sprintf("%s", minAmountThreshold)) + } + if len(params) > 0 { + query = query + "?" + params.Encode() + } + + returnedData, err := s.getRequest(query) + if err != nil { + return nil, err + } + var vaultAccounts []VaultAccount + err = json.Unmarshal([]byte(returnedData), &vaultAccounts) + if err != nil { + log.Error(err) + return nil, err + } + + return vaultAccounts, nil +} + +// GetVaultAccount - retrieve the vault account for the specified id. + +func (s *SDK) GetVaultAccount(vaultAccountID string) (VaultAccount, error) { + + query := fmt.Sprintf("/v1/vault/accounts/%s", vaultAccountID) + + returnedData, err := s.getRequest(query) + if err != nil { + return VaultAccount{}, err + } + + var vaultAccount VaultAccount + err = json.Unmarshal([]byte(returnedData), &vaultAccount) + if err != nil { + log.Error(err) + return VaultAccount{}, err + } + + if vaultAccount.Id == "" { + return VaultAccount{}, errors.New(returnedData) + } + + return vaultAccount, err + +} + +// GetVaultAccountAsset - Gets a single vault account asset +func (s *SDK) GetVaultAccountAsset(vaultAccountID string, assetID string) (VaultAsset, error) { + + query := fmt.Sprintf("/v1/vault/accounts/%s/%s", vaultAccountID, assetID) + + var vaultAsset VaultAsset + returnedData, err := s.getRequest(query) + if err != nil { + log.Error(err) + } + err = json.Unmarshal([]byte(returnedData), &vaultAsset) + if err != nil { + log.Errorf("failed to parse payload: %s. %v", returnedData, err) + return VaultAsset{}, err + } + return vaultAsset, err + +} + +// GetAddresses - Gets deposit addresses for an asset in a vault account +func (s *SDK) GetAddresses(vaultAccountID string, assetID string) ([]VaultAccountAssetAddress, error) { + + query := fmt.Sprintf("/v1/vault/accounts/%s/%s/addresses", vaultAccountID, assetID) + returnedData, err := s.getRequest(query) + if err != nil { + return nil, err + } + + var assetAddress []VaultAccountAssetAddress + err = json.Unmarshal([]byte(returnedData), &assetAddress) + if err != nil { + log.Errorf("failed to parse payload: %s. %v", returnedData, err) + return nil, err + } + + return assetAddress, nil + +} + +// GetUnspentInputs - Gets utxo list for an asset in a vault account +func (s *SDK) GetUnspentInputs(vaultAccountID string, assetID string) (string, error) { + query := fmt.Sprintf("/v1/vault/accounts/%s/%s/unspent_inouts", vaultAccountID, assetID) + return s.getRequest(query) +} + +// GenerateNewAddress - Generates a new address for an asset in a vault account +func (s *SDK) GenerateNewAddress(vaultAccountID string, assetID string, description string, customerRefID string, + idempotencyKey string, +) (CreateAddressResponse, error) { + query := fmt.Sprintf("/v1/vault/accounts/%s/%s/addresses", vaultAccountID, assetID) + + payload := make(map[string]interface{}) + + if len(description) > 0 { + payload["description"] = description + } + if len(customerRefID) > 0 { + payload["customerRefId"] = customerRefID + } + marshalled, err := json.Marshal(payload) + if err != nil { + return CreateAddressResponse{}, err + } + + returnedData, err := s.changeRequest(query, marshalled, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + log.Errorf("returned payload: %s", returnedData) + return CreateAddressResponse{}, err + } + + var createdAddress CreateAddressResponse + err = json.Unmarshal([]byte(returnedData), &createdAddress) + if err != nil { + log.Errorf("failed to parse payload: %s. %v", returnedData, err) + return CreateAddressResponse{}, err + } + + return createdAddress, nil + +} + +// SetAddressDescription - Sets the description of an existing address +func (s *SDK) SetAddressDescription(vaultAccountID string, assetID string, description string, address string, tag string) (string, error) { + + payload := make(map[string]interface{}) + + if len(description) > 0 { + payload["description"] = description + } + var query string + if len(tag) > 0 { + query = fmt.Sprintf("/v1/vault/accounts/%s/%s/addresses/%s:%s", vaultAccountID, assetID, address, tag) + } else { + query = fmt.Sprintf("/v1/vault/accounts/%s/%s/addresses/%s", vaultAccountID, assetID, address) + } + marshalled, err := json.Marshal(payload) + if err != nil { + return "", err + } + return s.changeRequest(query, marshalled, "", http.MethodPut) +} + +// GetNetworkConnections - Gets all network connections for your tenant +func (s *SDK) GetNetworkConnections() (string, error) { + return s.getRequest("/v1/network_connections") +} + +// GetNetworkConnectionByID - Gets a single network connection by id +func (s *SDK) GetNetworkConnectionByID(connectionID string) (string, error) { + query := fmt.Sprintf("/v1/network_connections/%s", connectionID) + return s.getRequest(query) +} + +// GetExchangeAccounts - Gets all exchange accounts for your tenant +func (s *SDK) GetExchangeAccounts() ([]ExchangeAccount, error) { + + returnedData, err := s.getRequest("/v1/exchange_accounts") + if err != nil { + log.Error(err) + } + var exchangeAccounts []ExchangeAccount + err = json.Unmarshal([]byte(returnedData), &exchangeAccounts) + if err != nil { + log.Error(err) + return nil, err + } + return exchangeAccounts, nil + +} + +// GetExchangeAccount - Gets an exchange account for your tenant +func (s *SDK) GetExchangeAccount(exchangeID string) (ExchangeAccount, error) { + + query := fmt.Sprintf("/v1/exchange_accounts/%s", exchangeID) + + returnedData, err := s.getRequest(query) + if err != nil { + log.Error(err) + return ExchangeAccount{}, err + } + + var exchangeAccount ExchangeAccount + err = json.Unmarshal([]byte(returnedData), &exchangeAccount) + if err != nil { + log.Error(err) + return ExchangeAccount{}, err + } + + return exchangeAccount, nil + +} + +// GetExchangeAccountAsset - Get a specific asset from an exchange account +func (s *SDK) GetExchangeAccountAsset(exchangeID string, assetID string) (ExchangeAsset, error) { + + query := fmt.Sprintf("/v1/exchange_accounts/%s/%s", exchangeID, assetID) + + returnedData, err := s.getRequest(query) + if err != nil { + log.Error(err) + return ExchangeAsset{}, err + } + + var exchangeAsset ExchangeAsset + err = json.Unmarshal([]byte(returnedData), &exchangeAsset) + if err != nil { + log.Error(err) + return ExchangeAsset{}, err + } + + return exchangeAsset, nil + +} + +func (s *SDK) SetCustomerRefId(vaultAccountId string, customerRefId string, idempotencyKey string) error { + + payload := map[string]interface{}{ + "customerRefId": customerRefId, + } + + marshalled, err := json.Marshal(payload) + if err != nil { + return err + } + + query := fmt.Sprintf("/v1/vault/accounts/%s", vaultAccountId) + _, err = s.changeRequest(query, marshalled, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + return err + } + + return nil + +} + +//POST /v1/webhooks/resend + +func (s *SDK) ResendFailedWebhookEvents() error { + + _, err := s.changeRequest("/v1/webhooks/resend", nil, "", http.MethodPost) + if err != nil { + log.Error(err) + } + return err + +} + +// CreateVaultAccount +// name - vaultaccount name - usually we use as a join of userid + product_id (XXXX_YYYY) +func (s *SDK) CreateVaultAccount(name string, hiddenOnUI bool, customerRefID string, autoFuel bool, idempotencyKey string) (VaultAccount, error) { + + payload := map[string]interface{}{ + "name": name, + "hiddenOnUI": hiddenOnUI, + "autoFuel": autoFuel, + } + + if len(customerRefID) > 0 { + payload["customerRefId"] = customerRefID + } + marshalled, err := json.Marshal(payload) + if err != nil { + return VaultAccount{}, err + } + + returnedData, err := s.changeRequest("/v1/vault/accounts", marshalled, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + } + var vaultAccount VaultAccount + err = json.Unmarshal([]byte(returnedData), &vaultAccount) + if err != nil { + log.Error(err) + } + + if vaultAccount.Id == "" { + return vaultAccount, errors.New(returnedData) + } + + return vaultAccount, err + +} + +//CreateVaultAsset +// creates a new wallet under the VaultAccount +// args: +// vaultAccountId +// assetId +func (s *SDK) CreateVaultAsset(vaultAccountId string, assetId string, idempotencyKey string) (CreateVaultAssetResponse, error) { + + cmd := fmt.Sprintf("/v1/vault/accounts/%s/%s", vaultAccountId, assetId) + + var createVaultAssetResponse CreateVaultAssetResponse + returnedData, err := s.changeRequest(cmd, nil, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + } + + err = json.Unmarshal([]byte(returnedData), &createVaultAssetResponse) + if err != nil { + log.Error(err) + } + + if createVaultAssetResponse.Id == "" { + return createVaultAssetResponse, errors.New(returnedData) + } + + return createVaultAssetResponse, err + +} + +// CreateExternalWallet +// customerRefId - used for identifying our clients. +func (s *SDK) CreateExternalWallet(name string, customerRefId string, idempotencyKey string) (ExternalWallet, error) { + + payload := map[string]interface{}{ + "name": name, + } + + if len(customerRefId) > 0 { + payload["customerRefId"] = customerRefId + } + marshalled, err := json.Marshal(payload) + if err != nil { + log.Error(err) + return ExternalWallet{}, err + } + + returnedData, err := s.changeRequest("/v1/external_wallets", marshalled, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + return ExternalWallet{}, err + } + + var externalWallet ExternalWallet + err = json.Unmarshal([]byte(returnedData), &externalWallet) + if err != nil { + log.Error(err) + return ExternalWallet{}, err + } + + return externalWallet, nil + +} + +func (s *SDK) CreateExternalWalletAsset(walletId string, assetId string, address string, tag string, idempotencyKey string) (ExternalWalletAsset, error) { + + cmd := fmt.Sprintf("/v1/external_wallets/%s/%s", walletId, assetId) + + payload := map[string]interface{}{ + "address": address, + } + + if len(tag) > 0 { + payload["tag"] = tag + } + marshalled, err := json.Marshal(payload) + if err != nil { + return ExternalWalletAsset{}, err + } + + returnedData, err := s.changeRequest(cmd, marshalled, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + return ExternalWalletAsset{}, err + } + var extWalletAsset ExternalWalletAsset + + err = json.Unmarshal([]byte(returnedData), &extWalletAsset) + if err != nil { + log.Error(err) + return ExternalWalletAsset{}, err + } + + return extWalletAsset, nil + +} + +//CreateInternalWallet + +func (s *SDK) CreateInternalWallet(name string, customerRefId string, idempotencyKey string) (UnmanagedWallet, error) { + + payload := map[string]interface{}{ + "name": name, + "customerRefId": customerRefId, + } + marshalled, err := json.Marshal(payload) + if err != nil { + return UnmanagedWallet{}, err + } + + returnedData, err := s.changeRequest("/v1/internal_wallets", marshalled, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + return UnmanagedWallet{}, err + } + var unmanagedWallet UnmanagedWallet + err = json.Unmarshal([]byte(returnedData), &unmanagedWallet) + if err != nil { + log.Error(err) + return UnmanagedWallet{}, err + } + + return unmanagedWallet, nil +} + +// CreateInternalWalletAsset + +func (s *SDK) CreateInternalWalletAsset(walletId string, assetId string, address string, tag string, idempotencyKey string) (WalletAsset, error) { + + cmd := fmt.Sprintf("/v1/internal_wallets/%s/%s", walletId, assetId) + payload := map[string]interface{}{ + "address": address, + } + if len(tag) > 0 { + payload["tag"] = tag + } + marshalled, err := json.Marshal(payload) + if err != nil { + return WalletAsset{}, err + } + + returnedData, err := s.changeRequest(cmd, marshalled, idempotencyKey, http.MethodPost) + if err != nil { + log.Error(err) + return WalletAsset{}, err + } + var walletAsset WalletAsset + err = json.Unmarshal([]byte(returnedData), &walletAsset) + if err != nil { + log.Error(err) + return WalletAsset{}, err + } + + return walletAsset, nil + +} + +//GetEstimateTxFee +// Get the estimate fee for a tx. +func (s *SDK) GetEstimateTxFee(assetId string, amount string, source TransferPeerPath, destination DestinationTransferPeerPath, operation string) (EstimatedTransactionFeeResponse, error) { + + payload := map[string]interface{}{ + "assetId": assetId, + "amount": amount, + "source": source, + "destination": destination, + "operation": operation, + } + + marshalled, err := json.Marshal(payload) + if err != nil { + return EstimatedTransactionFeeResponse{}, err + } + + returnedData, err := s.changeRequest("/v1/transactions/estimate_fee", marshalled, "", http.MethodPost) + if err != nil { + log.Error(err) + return EstimatedTransactionFeeResponse{}, err + } + + if strings.Contains(returnedData, "message") { + // {"message":"The asset is not supported by Fireblocks, please check the supported assets endpoint.","code":1025} + errMsg := fmt.Sprintf("Request failed: %s", returnedData) + return EstimatedTransactionFeeResponse{}, errors.New(errMsg) + } + + var estimatedTxFee EstimatedTransactionFeeResponse + err = json.Unmarshal([]byte(returnedData), &estimatedTxFee) + if err != nil { + log.Error(err) + return EstimatedTransactionFeeResponse{}, err + } + + return estimatedTxFee, nil + +} + +// CreateTransaction - +func (s *SDK) CreateTransaction(assetId string, amount decimal.Decimal, source TransferPeerPath, + destination DestinationTransferPeerPath, fee decimal.Decimal, gasPrice decimal.Decimal, waitForStatus bool, + txType TransactionType, note string, cpuStaking string, networkStaking string, + autoStaking string, customerRefId string, extraParams ExtraParameters, destinations []DestinationTransferPeerPath, + feeLevel FeeLevel, failOnFee bool, maxFee string, gasLimit decimal.Decimal, replaceTxByHash string, idempotencyKey string, + +) (CreateTransactionResponse, error) { + + payload := CreateTransactionPayload{ + AssetId: assetId, + Source: source, + Destination: destination, + Amount: amount.String(), + TreatAsGrossAmount: false, + FailOnLowFee: false, + Operation: string(txType), + WaitForStatus: waitForStatus, + } + + if fee.IsPositive() { + payload.Fee = fee.String() + } + + if len(feeLevel) > 0 { + payload.FeeLevel = string(feeLevel) + } + + if len(note) > 0 { + payload.Note = note + } + + if len(maxFee) > 0 { + payload.MaxFee = maxFee + } + + if gasPrice.IsPositive() { + payload.GasPrice = gasPrice.String() + } + + if gasLimit.IsPositive() { + payload.GasLimit = gasLimit.String() + } + + if len(cpuStaking) > 0 { + payload.CpuStaking = cpuStaking + } + + if len(networkStaking) > 0 { + payload.NetworkStaking = networkStaking + } + + if len(autoStaking) > 0 { + payload.AutoStaking = autoStaking + } + if len(customerRefId) > 0 { + payload.CustomerRefId = customerRefId + } + + if len(replaceTxByHash) > 0 { + payload.ReplacedTxHash = replaceTxByHash + } + + if extraParams != (ExtraParameters{}) { + payload.ExtraParameters = extraParams + } + + if len(destinations) > 0 { + payload.Destinations = destinations + } + + return s.CreateTransactionWithPayload(payload, idempotencyKey) +} + +func (s *SDK) CreateTransactionWithPayload(payload CreateTransactionPayload, idempotencyKey string) (CreateTransactionResponse, error) { + + marshalled, err := json.Marshal(payload) + if err != nil { + return CreateTransactionResponse{}, err + } + returnedData, err := s.changeRequest("/v1/transactions", marshalled, idempotencyKey, http.MethodPost) + + if err != nil { + log.Error(err) + } + + var transactionResponse CreateTransactionResponse + err = json.Unmarshal([]byte(returnedData), &transactionResponse) + if err != nil { + log.Error(err) + return CreateTransactionResponse{}, errors.New(returnedData) + } + + return transactionResponse, err +} + +func (s *SDK) GetVaultAssetsBalance(accountNamePrefix string, accountNameSuffix string) (string, error) { + + params := url.Values{} + if len(accountNamePrefix) > 0 { + params.Add("accountNamePrefix", accountNamePrefix) + } + if len(accountNameSuffix) > 0 { + params.Add("accountNameSuffix", accountNameSuffix) + } + uri := "/v1/vault/assets" + if len(params) > 0 { + query := fmt.Sprintf(uri+"?%s", params.Encode()) + return s.getRequest(query) + } else { + return s.getRequest(uri) + } +} + +func (s *SDK) GetVaultBalanceByAsset(assetId string) (string, error) { + + query := fmt.Sprintf("/v1/vault/assets/%s", assetId) + return s.getRequest(query) +} + +// ValidateAddress - validates the address of a given asset. +// assetId - the id of the asset to validate the address +// address - the address to validate +func (s *SDK) ValidateAddress(assetId string, address string) (AddressStatus, error) { + + query := fmt.Sprintf("/v1/transactions/validate_address/%s/%s", assetId, address) + + returnedData, err := s.getRequest(query) + if err != nil { + log.Error(err) + return AddressStatus{}, err + } + var addressStatus AddressStatus + err = json.Unmarshal([]byte(returnedData), &addressStatus) + if err != nil { + log.Error(err) + return AddressStatus{}, err + } + + return addressStatus, nil +} + +// GetTransactionById - get the transaction details +// txId - transaction id +func (s *SDK) GetTransactionById(txId string) (TransactionDetails, error) { + + query := fmt.Sprintf("/v1/transactions/%s", txId) + returnedData, err := s.getRequest(query) + if err != nil { + log.Error(err) + return TransactionDetails{}, err + } + var transactionDetails TransactionDetails + err = json.Unmarshal([]byte(returnedData), &transactionDetails) + if err != nil { + log.Error(err) + return TransactionDetails{}, err + } + + return transactionDetails, nil + +} + +func (s *SDK) GetExternalWallets() ([]ExternalWallet, error) { + + returnedData, err := s.getRequest("/v1/external_wallets") + if err != nil { + log.Error(err) + return nil, err + } + + var extWallets []ExternalWallet + err = json.Unmarshal([]byte(returnedData), &extWallets) + if err != nil { + log.Error(err) + return nil, err + } + + return extWallets, nil + +} + +func (s *SDK) GetExternalWallet(externalWalletId string) (ExternalWallet, error) { + + query := fmt.Sprintf("/v1/external_wallets/%s", externalWalletId) + + returnedData, err := s.getRequest(query) + if err != nil { + log.Error(err) + return ExternalWallet{}, err + } + + extWallet, err2 := getExtWallet(returnedData) + if err2 != nil { + return ExternalWallet{}, err2 + } + + return extWallet, nil + +} + +func getExtWallet(returnedData string) (ExternalWallet, error) { + var extWallet ExternalWallet + err := json.Unmarshal([]byte(returnedData), &extWallet) + if err != nil { + log.Error(err) + return ExternalWallet{}, err + } + return extWallet, nil +} diff --git a/cmd/keycmd/fireblocks/keychain.go b/cmd/keycmd/fireblocks/keychain.go new file mode 100644 index 000000000..fae43e08e --- /dev/null +++ b/cmd/keycmd/fireblocks/keychain.go @@ -0,0 +1,78 @@ +package fireblocks + +import ( + "io" + "os" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/keychain" + "github.com/ava-labs/avalanchego/utils/set" +) + +var ( + _ keychain.Keychain = &FireblocksKeychain{} + _ keychain.Signer = &FireblocksSigner{} +) + +type FireblocksKeychain struct { + pk string + ak string + vault string +} + +type FireblocksSigner struct { + sdk *SDK + vault string +} + +func NewFireblocksKeychain(pk, ak, vault string) (*FireblocksKeychain, error) { + return &FireblocksKeychain{pk, ak, vault}, nil +} + +// The returned Signer can provide a signature for [addr] +func (fk *FireblocksKeychain) Get(addr ids.ShortID) (keychain.Signer, bool) { + signer, err := NewFireblocksSigner(fk.pk, fk.ak, fk.vault) + if err != nil { + return nil, false + } + return signer, true +} + +// Returns the set of addresses for which the accessor keeps an associated +// signer +func (*FireblocksKeychain) Addresses() set.Set[ids.ShortID] { + s := set.NewSet[ids.ShortID](1) + s.Add(ids.ShortEmpty) + return s +} + +func NewFireblocksSigner(pk, ak, vault string) (*FireblocksSigner, error) { + f, err := os.Open(pk) + if err != nil { + return nil, err + } + defer f.Close() + + pkBytes, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + return &FireblocksSigner{ + NewInstance(pkBytes, ak, "https://sandbox-api.fireblocks.io", time.Hour), + vault, + }, nil +} + +func (*FireblocksSigner) SignHash([]byte) ([]byte, error) { + panic("impelement me") +} + +func (*FireblocksSigner) Sign([]byte) ([]byte, error) { + panic("impelement me") +} + +func (*FireblocksSigner) Address() ids.ShortID { + panic("doesn't support") +} diff --git a/cmd/keycmd/transfer.go b/cmd/keycmd/transfer.go index e7e4a57c7..b1456ac99 100644 --- a/cmd/keycmd/transfer.go +++ b/cmd/keycmd/transfer.go @@ -8,6 +8,7 @@ import ( "math/big" "time" + "github.com/ava-labs/avalanche-cli/cmd/keycmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/contract" clievm "github.com/ava-labs/avalanche-cli/pkg/evm" @@ -244,11 +245,32 @@ func transferF(*cobra.Command, []string) error { var kc keychain.Keychain var sk *key.SoftKey if keyName != "" { - sk, err = app.GetKey(keyName, network, false) - if err != nil { - return err + if keyName == "fireblocks" { + fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") + if err != nil { + return err + } + + fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") + if err != nil { + return err + } + + fireblocksVn, err := app.Prompt.CaptureString("Press enter vault name") + if err != nil { + return err + } + kc, err = fireblocks.NewFireblocksKeychain(fireblocksPk, fireblocksAk, fireblocksVn) + if err != nil { + return err + } + } else { + sk, err = app.GetKey(keyName, network, false) + if err != nil { + return err + } + kc = sk.KeyChain() } - kc = sk.KeyChain() } else { ledgerDevice, err := ledger.New() if err != nil { diff --git a/go.mod b/go.mod index 8a3107b4a..833293a3c 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,8 @@ require ( github.com/ethereum/go-ethereum v1.13.14 github.com/fatih/color v1.18.0 github.com/go-git/go-git/v5 v5.13.1 + github.com/gojek/heimdall/v7 v7.0.2 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/jedib0t/go-pretty/v6 v6.6.5 github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 @@ -36,6 +38,8 @@ require ( github.com/prometheus/client_golang v1.20.5 github.com/schollz/progressbar/v3 v3.17.1 github.com/shirou/gopsutil v3.21.11+incompatible + github.com/shopspring/decimal v1.2.0 + github.com/sirupsen/logrus v1.9.0 github.com/spf13/afero v1.12.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 @@ -60,6 +64,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect dario.cat/mergo v1.0.0 // indirect + github.com/DataDog/datadog-go v3.7.1+incompatible // indirect github.com/DataDog/zstd v1.5.2 // indirect github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect @@ -67,6 +72,7 @@ require ( github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect + github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/ava-labs/icm-contracts v1.0.9-0.20250204232902-ae24f1f2636f // indirect github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60 // indirect @@ -86,6 +92,7 @@ require ( github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect + github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -127,6 +134,7 @@ require ( github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -189,6 +197,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/cors v1.7.0 // indirect @@ -196,7 +205,6 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect github.com/skeema/knownhosts v1.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.6.0 // indirect diff --git a/go.sum b/go.sum index e069ec58f..049849683 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.7.1+incompatible h1:HmA9qHVrHIAqpSvoCYJ+c6qst0lgqEhNW6/KwfkHbS8= +github.com/DataDog/datadog-go v3.7.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= @@ -68,6 +70,8 @@ github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6Ro github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -170,6 +174,8 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c h1:HIGF0r/56+7fuIZw2V4isE22MK6xpxWx7BbV8dJ290w= +github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4= github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -385,6 +391,10 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/gojek/heimdall/v7 v7.0.2 h1:+YutGXZ8oEWbCJIwjRnkKmoTl+Oxt1Urs3hc/FR0sxU= +github.com/gojek/heimdall/v7 v7.0.2/go.mod h1:Z43HtMid7ysSjmsedPTXAki6jcdcNVnjn5pmsTyiMic= +github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf h1:5xRGbUdOmZKoDXkGx5evVLehuCMpuO1hl701bEQqXOM= +github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf/go.mod h1:QzhUKaYKJmcbTnCYCAVQrroCOY7vOOI8cSQ4NbuhYf0= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= @@ -470,6 +480,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -550,6 +561,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b h1:FQ7+9fxhyp82ks9vAuyPzG0/vVbWwMwLJ+P6yJI5FN8= github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks= @@ -766,6 +778,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -801,6 +815,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -808,7 +824,9 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0 github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= @@ -836,6 +854,7 @@ github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobt github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= diff --git a/pkg/utils/keys.go b/pkg/utils/keys.go index 753953d93..3debae55c 100644 --- a/pkg/utils/keys.go +++ b/pkg/utils/keys.go @@ -40,6 +40,7 @@ func GetKeyNames(keyDir string, addEwoq bool) ([]string, error) { } if addEwoq { userKeys = append(userKeys, "ewoq") + userKeys = append(userKeys, "fireblocks") } names = append(append(userKeys, subnetKeys...), cliKeys...) return names, nil From dea4a08d3e1fdac50e3491e979bb157664fb2a92 Mon Sep 17 00:00:00 2001 From: Ilnur Date: Thu, 13 Mar 2025 16:18:16 +0400 Subject: [PATCH 02/15] add keychain implementation --- cmd/keycmd/fireblocks/fireblocks_sdk.go | 100 +++++++++++++++++++++--- cmd/keycmd/fireblocks/keychain.go | 81 ++++++++++++++----- cmd/keycmd/fireblocks/keychain_test.go | 13 +++ cmd/keycmd/transfer.go | 10 ++- 4 files changed, 170 insertions(+), 34 deletions(-) create mode 100644 cmd/keycmd/fireblocks/keychain_test.go diff --git a/cmd/keycmd/fireblocks/fireblocks_sdk.go b/cmd/keycmd/fireblocks/fireblocks_sdk.go index f6d10a41a..f7e5c63a1 100644 --- a/cmd/keycmd/fireblocks/fireblocks_sdk.go +++ b/cmd/keycmd/fireblocks/fireblocks_sdk.go @@ -11,10 +11,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gojek/heimdall/v7/hystrix" - "github.com/golang-jwt/jwt" - "github.com/shopspring/decimal" - log "github.com/sirupsen/logrus" "io" "io/ioutil" "math/rand" @@ -22,6 +18,11 @@ import ( "net/url" "strings" "time" + + "github.com/gojek/heimdall/v7/hystrix" + "github.com/golang-jwt/jwt" + "github.com/shopspring/decimal" + log "github.com/sirupsen/logrus" ) type FbKeyMgmt struct { @@ -255,7 +256,7 @@ func (s *SDK) GetSupportedAssets() ([]AssetTypeResponse, error) { // GetVaultAccounts - gets all vault accounts for the tenant. func (s *SDK) GetVaultAccounts(namePrefix string, nameSuffix string, minAmountThreshold decimal.Decimal) ([]VaultAccount, error) { - query := "/v1/vault/accounts" + query := "/v1/vault/accounts_paged" params := url.Values{} if namePrefix != "" { @@ -275,14 +276,16 @@ func (s *SDK) GetVaultAccounts(namePrefix string, nameSuffix string, minAmountTh if err != nil { return nil, err } - var vaultAccounts []VaultAccount + var vaultAccounts struct { + Accounts []VaultAccount `json:"accounts"` + } err = json.Unmarshal([]byte(returnedData), &vaultAccounts) if err != nil { log.Error(err) return nil, err } - return vaultAccounts, nil + return vaultAccounts.Accounts, nil } // GetVaultAccount - retrieve the vault account for the specified id. @@ -556,11 +559,12 @@ func (s *SDK) CreateVaultAccount(name string, hiddenOnUI bool, customerRefID str } -//CreateVaultAsset +// CreateVaultAsset // creates a new wallet under the VaultAccount // args: -// vaultAccountId -// assetId +// +// vaultAccountId +// assetId func (s *SDK) CreateVaultAsset(vaultAccountId string, assetId string, idempotencyKey string) (CreateVaultAssetResponse, error) { cmd := fmt.Sprintf("/v1/vault/accounts/%s/%s", vaultAccountId, assetId) @@ -711,7 +715,7 @@ func (s *SDK) CreateInternalWalletAsset(walletId string, assetId string, address } -//GetEstimateTxFee +// GetEstimateTxFee // Get the estimate fee for a tx. func (s *SDK) GetEstimateTxFee(assetId string, amount string, source TransferPeerPath, destination DestinationTransferPeerPath, operation string) (EstimatedTransactionFeeResponse, error) { @@ -914,6 +918,80 @@ func (s *SDK) GetTransactionById(txId string) (TransactionDetails, error) { } +func (s *SDK) SignData(vaultid string, assetId string, data []byte) ([]byte, []byte, error) { + req, err := json.Marshal(map[string]any{ + "source": map[string]any{ + "type": "VAULT_ACCOUNT", + "id": vaultid, + }, + "assetId": assetId, + "operation": "RAW", + "extraParameters": map[string]any{ + "rawMessageData": map[string]any{ + "messages": []map[string]any{ + { + "content": hex.EncodeToString(data), + }, + }, + }, + }, + }) + if err != nil { + return nil, nil, err + } + + resp, err := s.changeRequest("/v1/transactions", req, "", http.MethodPost) + if err != nil { + return nil, nil, err + } + + var tx struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(resp), &tx); err != nil { + return nil, nil, err + } + + var ( + receipt struct { + Status TransactionStatus `json:"status"` + SignedMessages []struct { + PublicKey string `json:"publicKey"` + Signature struct { + FullSig string `json:"fullSig"` + } + } `json:"signedMessages"` + } + complete bool + ) + for !complete { + time.Sleep(500 * time.Millisecond) + returnedData, err := s.getRequest(fmt.Sprintf("/v1/transactions/%s", tx.ID)) + if err != nil { + return nil, nil, err + } + if err := json.Unmarshal([]byte(returnedData), &receipt); err != nil { + return nil, nil, err + } + complete = receipt.Status == "COMPLETED" || receipt.Status == "FAILED" + } + + if len(receipt.SignedMessages) != 1 { + return nil, nil, fmt.Errorf("signatures not found") + } + + rawPublicKey, err := hex.DecodeString(receipt.SignedMessages[0].PublicKey) + if err != nil { + return nil, nil, err + } + + rawSignature, err := hex.DecodeString(receipt.SignedMessages[0].Signature.FullSig) + if err != nil { + return nil, nil, err + } + return rawSignature, rawPublicKey, nil +} + func (s *SDK) GetExternalWallets() ([]ExternalWallet, error) { returnedData, err := s.getRequest("/v1/external_wallets") diff --git a/cmd/keycmd/fireblocks/keychain.go b/cmd/keycmd/fireblocks/keychain.go index fae43e08e..db496e760 100644 --- a/cmd/keycmd/fireblocks/keychain.go +++ b/cmd/keycmd/fireblocks/keychain.go @@ -1,12 +1,16 @@ package fireblocks import ( + "encoding/hex" "io" "os" + "sync" "time" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/keychain" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/set" ) @@ -16,38 +20,46 @@ var ( ) type FireblocksKeychain struct { - pk string - ak string - vault string + signer *FireblocksSigner } type FireblocksSigner struct { - sdk *SDK - vault string + sdk *SDK + vaultid string + assetid string + + addr ids.ShortID + mu sync.Mutex } -func NewFireblocksKeychain(pk, ak, vault string) (*FireblocksKeychain, error) { - return &FireblocksKeychain{pk, ak, vault}, nil +func NewFireblocksKeychain(pk, ak, vaultid, assetid string) (*FireblocksKeychain, error) { + signer, err := NewFireblocksSigner(pk, ak, vaultid, assetid) + if err != nil { + return nil, err + } + + return &FireblocksKeychain{ + signer: signer, + }, nil } // The returned Signer can provide a signature for [addr] func (fk *FireblocksKeychain) Get(addr ids.ShortID) (keychain.Signer, bool) { - signer, err := NewFireblocksSigner(fk.pk, fk.ak, fk.vault) - if err != nil { + if fk.signer.Address().Compare(addr) != 0 { return nil, false } - return signer, true + return fk.signer, true } // Returns the set of addresses for which the accessor keeps an associated // signer -func (*FireblocksKeychain) Addresses() set.Set[ids.ShortID] { +func (fk *FireblocksKeychain) Addresses() set.Set[ids.ShortID] { s := set.NewSet[ids.ShortID](1) - s.Add(ids.ShortEmpty) + s.Add(fk.signer.Address()) return s } -func NewFireblocksSigner(pk, ak, vault string) (*FireblocksSigner, error) { +func NewFireblocksSigner(pk, ak, vaultid, assetid string) (*FireblocksSigner, error) { f, err := os.Open(pk) if err != nil { return nil, err @@ -60,19 +72,46 @@ func NewFireblocksSigner(pk, ak, vault string) (*FireblocksSigner, error) { } return &FireblocksSigner{ - NewInstance(pkBytes, ak, "https://sandbox-api.fireblocks.io", time.Hour), - vault, + sdk: NewInstance(pkBytes, ak, "https://sandbox-api.fireblocks.io", time.Hour), + vaultid: vaultid, + assetid: assetid, + + addr: ids.ShortEmpty, + mu: sync.Mutex{}, }, nil } -func (*FireblocksSigner) SignHash([]byte) ([]byte, error) { - panic("impelement me") +func (fs *FireblocksSigner) SignHash(hash []byte) ([]byte, error) { + sig, _, err := fs.sdk.SignData(fs.vaultid, fs.assetid, hash) + return sig, err } -func (*FireblocksSigner) Sign([]byte) ([]byte, error) { - panic("impelement me") +func (fs *FireblocksSigner) Sign(data []byte) ([]byte, error) { + return fs.SignHash(hashing.ComputeHash256(data)) } -func (*FireblocksSigner) Address() ids.ShortID { - panic("doesn't support") +func (fs *FireblocksSigner) Address() ids.ShortID { + fs.mu.Lock() + defer fs.mu.Unlock() + + if fs.addr.Compare(ids.ShortEmpty) == 0 { + msg, err := hex.DecodeString("802a5a961895b3f8c6556e31d0960a5778d7135be7d04bbbadd5e406c4bac381") + if err != nil { + panic(err) + } + + _, rawpb, err := fs.sdk.SignData(fs.vaultid, fs.assetid, msg) + if err != nil { + panic(err) + } + + pb, err := secp256k1.ToPublicKey(rawpb) + if err != nil { + panic(err) + } + + fs.addr = pb.Address() + } + + return fs.addr } diff --git a/cmd/keycmd/fireblocks/keychain_test.go b/cmd/keycmd/fireblocks/keychain_test.go new file mode 100644 index 000000000..3c3cbe5eb --- /dev/null +++ b/cmd/keycmd/fireblocks/keychain_test.go @@ -0,0 +1,13 @@ +package fireblocks + +import "testing" + +func TestSigner(t *testing.T) { + signer, err := NewFireblocksSigner("/Users/n0cte/Downloads/fireblocks_secret_editor_e4fafe6f-742f-423c-b5fa-2af197e932d8.key", "e4fafe6f-742f-423c-b5fa-2af197e932d8", "219", "AVAXTEST") + if err != nil { + t.Fatal(err) + } + address := signer.Address() + straddr := address.String() + t.Logf("Signer: %s %s", address, straddr) +} diff --git a/cmd/keycmd/transfer.go b/cmd/keycmd/transfer.go index b1456ac99..8868cf5cf 100644 --- a/cmd/keycmd/transfer.go +++ b/cmd/keycmd/transfer.go @@ -256,11 +256,17 @@ func transferF(*cobra.Command, []string) error { return err } - fireblocksVn, err := app.Prompt.CaptureString("Press enter vault name") + fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") if err != nil { return err } - kc, err = fireblocks.NewFireblocksKeychain(fireblocksPk, fireblocksAk, fireblocksVn) + + fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") + if err != nil { + return err + } + + kc, err = fireblocks.NewFireblocksKeychain(fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) if err != nil { return err } From fa5981ead8aabf32182ccdec062e96049776daf4 Mon Sep 17 00:00:00 2001 From: Ilnur Date: Fri, 14 Mar 2025 15:38:23 +0400 Subject: [PATCH 03/15] add fireblocks cmd --- cmd/fireblockscmd/addresscmd.go | 42 +++++++++++++++++++ .../fireblocks/fireblocks_domain.go | 0 .../fireblocks/fireblocks_sdk.go | 0 .../fireblocks/keychain.go | 8 ++-- cmd/fireblockscmd/fireblocks/keychain_test.go | 13 ++++++ cmd/fireblockscmd/fireblockscmd.go | 22 ++++++++++ cmd/keycmd/fireblocks/keychain_test.go | 13 ------ cmd/keycmd/transfer.go | 9 +++- cmd/root.go | 4 ++ 9 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 cmd/fireblockscmd/addresscmd.go rename cmd/{keycmd => fireblockscmd}/fireblocks/fireblocks_domain.go (100%) rename cmd/{keycmd => fireblockscmd}/fireblocks/fireblocks_sdk.go (100%) rename cmd/{keycmd => fireblockscmd}/fireblocks/keychain.go (87%) create mode 100644 cmd/fireblockscmd/fireblocks/keychain_test.go create mode 100644 cmd/fireblockscmd/fireblockscmd.go delete mode 100644 cmd/keycmd/fireblocks/keychain_test.go diff --git a/cmd/fireblockscmd/addresscmd.go b/cmd/fireblockscmd/addresscmd.go new file mode 100644 index 000000000..bbd22fc4e --- /dev/null +++ b/cmd/fireblockscmd/addresscmd.go @@ -0,0 +1,42 @@ +package fireblockscmd + +import ( + "fmt" + + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/spf13/cobra" +) + +var ( + apiAddr string + priKey string + apiKey string + vaultId string + assetId string +) + +func newAddressCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "address", + Short: "show fireblocks avalanche short id", + RunE: address, + Args: cobrautils.ExactArgs(0), + Hidden: false, + } + cmd.Flags().StringVar(&apiAddr, "api-addr", "https://sandbox-api.fireblocks.io", "fireblocks api address") + cmd.Flags().StringVar(&priKey, "private-key", "/Users/n0cte/Downloads/fireblocks_secret_editor_e4fafe6f-742f-423c-b5fa-2af197e932d8.key", "absolute path to fireblocks api private key") + cmd.Flags().StringVar(&apiKey, "api-key", "e4fafe6f-742f-423c-b5fa-2af197e932d8", "fireblocks api key") + cmd.Flags().StringVar(&vaultId, "vault-id", "219", "fireblocks vault id") + cmd.Flags().StringVar(&assetId, "asset-id", "AVAXTEST", "fireblocks asset id") + return cmd +} + +func address(_ *cobra.Command, _ []string) error { + signer, err := fireblocks.NewFireblocksSigner(apiAddr, priKey, apiKey, vaultId, assetId) + if err != nil { + return err + } + fmt.Printf("ShortID: %s\n", signer.Address()) + return nil +} diff --git a/cmd/keycmd/fireblocks/fireblocks_domain.go b/cmd/fireblockscmd/fireblocks/fireblocks_domain.go similarity index 100% rename from cmd/keycmd/fireblocks/fireblocks_domain.go rename to cmd/fireblockscmd/fireblocks/fireblocks_domain.go diff --git a/cmd/keycmd/fireblocks/fireblocks_sdk.go b/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go similarity index 100% rename from cmd/keycmd/fireblocks/fireblocks_sdk.go rename to cmd/fireblockscmd/fireblocks/fireblocks_sdk.go diff --git a/cmd/keycmd/fireblocks/keychain.go b/cmd/fireblockscmd/fireblocks/keychain.go similarity index 87% rename from cmd/keycmd/fireblocks/keychain.go rename to cmd/fireblockscmd/fireblocks/keychain.go index db496e760..18b2709ff 100644 --- a/cmd/keycmd/fireblocks/keychain.go +++ b/cmd/fireblockscmd/fireblocks/keychain.go @@ -32,8 +32,8 @@ type FireblocksSigner struct { mu sync.Mutex } -func NewFireblocksKeychain(pk, ak, vaultid, assetid string) (*FireblocksKeychain, error) { - signer, err := NewFireblocksSigner(pk, ak, vaultid, assetid) +func NewFireblocksKeychain(apiAddr, pk, ak, vaultid, assetid string) (*FireblocksKeychain, error) { + signer, err := NewFireblocksSigner(apiAddr, pk, ak, vaultid, assetid) if err != nil { return nil, err } @@ -59,7 +59,7 @@ func (fk *FireblocksKeychain) Addresses() set.Set[ids.ShortID] { return s } -func NewFireblocksSigner(pk, ak, vaultid, assetid string) (*FireblocksSigner, error) { +func NewFireblocksSigner(apiAddr, pk, ak, vaultid, assetid string) (*FireblocksSigner, error) { f, err := os.Open(pk) if err != nil { return nil, err @@ -72,7 +72,7 @@ func NewFireblocksSigner(pk, ak, vaultid, assetid string) (*FireblocksSigner, er } return &FireblocksSigner{ - sdk: NewInstance(pkBytes, ak, "https://sandbox-api.fireblocks.io", time.Hour), + sdk: NewInstance(pkBytes, ak, apiAddr, time.Hour), vaultid: vaultid, assetid: assetid, diff --git a/cmd/fireblockscmd/fireblocks/keychain_test.go b/cmd/fireblockscmd/fireblocks/keychain_test.go new file mode 100644 index 000000000..c392e65ac --- /dev/null +++ b/cmd/fireblockscmd/fireblocks/keychain_test.go @@ -0,0 +1,13 @@ +package fireblocks + +import "testing" + +func TestSigner(t *testing.T) { + signer, err := NewFireblocksSigner("https://sandbox-api.fireblocks.io", "/Users/n0cte/Downloads/fireblocks_secret_editor_e4fafe6f-742f-423c-b5fa-2af197e932d8.key", "e4fafe6f-742f-423c-b5fa-2af197e932d8", "219", "AVAXTEST") + if err != nil { + t.Fatal(err) + } + address := signer.Address() + straddr := address.String() + t.Logf("Signer: %s %s", address, straddr) +} diff --git a/cmd/fireblockscmd/fireblockscmd.go b/cmd/fireblockscmd/fireblockscmd.go new file mode 100644 index 000000000..c9b8e81c2 --- /dev/null +++ b/cmd/fireblockscmd/fireblockscmd.go @@ -0,0 +1,22 @@ +package fireblockscmd + +import ( + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/spf13/cobra" +) + +var app *application.Avalanche + +func NewCmd(injectedApp *application.Avalanche) *cobra.Command { + app = injectedApp + + cmd := &cobra.Command{ + Use: "fireblocks", + Short: "Fireblocks helper functions", + RunE: cobrautils.CommandSuiteUsage, + } + + cmd.AddCommand(newAddressCmd()) + return cmd +} diff --git a/cmd/keycmd/fireblocks/keychain_test.go b/cmd/keycmd/fireblocks/keychain_test.go deleted file mode 100644 index 3c3cbe5eb..000000000 --- a/cmd/keycmd/fireblocks/keychain_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package fireblocks - -import "testing" - -func TestSigner(t *testing.T) { - signer, err := NewFireblocksSigner("/Users/n0cte/Downloads/fireblocks_secret_editor_e4fafe6f-742f-423c-b5fa-2af197e932d8.key", "e4fafe6f-742f-423c-b5fa-2af197e932d8", "219", "AVAXTEST") - if err != nil { - t.Fatal(err) - } - address := signer.Address() - straddr := address.String() - t.Logf("Signer: %s %s", address, straddr) -} diff --git a/cmd/keycmd/transfer.go b/cmd/keycmd/transfer.go index 8868cf5cf..d2892e49f 100644 --- a/cmd/keycmd/transfer.go +++ b/cmd/keycmd/transfer.go @@ -8,7 +8,7 @@ import ( "math/big" "time" - "github.com/ava-labs/avalanche-cli/cmd/keycmd/fireblocks" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/contract" clievm "github.com/ava-labs/avalanche-cli/pkg/evm" @@ -246,6 +246,11 @@ func transferF(*cobra.Command, []string) error { var sk *key.SoftKey if keyName != "" { if keyName == "fireblocks" { + fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") + if err != nil { + return err + } + fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") if err != nil { return err @@ -266,7 +271,7 @@ func transferF(*cobra.Command, []string) error { return err } - kc, err = fireblocks.NewFireblocksKeychain(fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + kc, err = fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index e2bc97306..022107a02 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,6 +13,7 @@ import ( "syscall" "time" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd" "github.com/ava-labs/avalanche-cli/cmd/validatorcmd" "github.com/ava-labs/avalanche-cli/cmd/backendcmd" @@ -89,6 +90,9 @@ in with avalanche blockchain create myNewBlockchain.`, // add hidden backend command rootCmd.AddCommand(backendcmd.NewCmd(app)) + // add hidden fireblocks command + rootCmd.AddCommand(fireblockscmd.NewCmd(app)) + // add transaction command rootCmd.AddCommand(transactioncmd.NewCmd(app)) From 049eb8550b4e3f723a7ea324a79f791a18f77b2b Mon Sep 17 00:00:00 2001 From: Ilnur Date: Fri, 14 Mar 2025 16:20:22 +0400 Subject: [PATCH 04/15] add fireblocks keychain into transaction_sign --- cmd/transactioncmd/transaction_sign.go | 42 ++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/cmd/transactioncmd/transaction_sign.go b/cmd/transactioncmd/transaction_sign.go index eeea80e68..30230ac8b 100644 --- a/cmd/transactioncmd/transaction_sign.go +++ b/cmd/transactioncmd/transaction_sign.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/keychain" @@ -149,9 +150,44 @@ func signTx(_ *cobra.Command, args []string) error { } // get keychain accessor - kc, err := keychain.GetKeychain(app, false, useLedger, ledgerAddresses, keyName, network, 0) - if err != nil { - return err + var kc *keychain.Keychain + if keyName == "fireblocks" { + fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") + if err != nil { + return err + } + + fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") + if err != nil { + return err + } + + fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") + if err != nil { + return err + } + + fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") + if err != nil { + return err + } + + fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") + if err != nil { + return err + } + + ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + if err != nil { + return err + } + + kc = keychain.NewKeychain(network, ckc, nil, nil) + } else { + kc, err = keychain.GetKeychain(app, false, useLedger, ledgerAddresses, keyName, network, 0) + if err != nil { + return err + } } // add control keys to the keychain whenever possible From 8db86b9cd924768aba2c10381d350b52aed6308d Mon Sep 17 00:00:00 2001 From: Ilnur Date: Fri, 14 Mar 2025 16:25:19 +0400 Subject: [PATCH 05/15] little fix for mainnet --- cmd/transactioncmd/transaction_sign.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/transactioncmd/transaction_sign.go b/cmd/transactioncmd/transaction_sign.go index 30230ac8b..64cea9964 100644 --- a/cmd/transactioncmd/transaction_sign.go +++ b/cmd/transactioncmd/transaction_sign.go @@ -100,7 +100,7 @@ func signTx(_ *cobra.Command, args []string) error { } } case models.Mainnet: - useLedger = true + useLedger = keyName != "fireblocks" if keyName != "" { return blockchaincmd.ErrStoredKeyOnMainnet } From 887dffc56560c755a2a509be2e225ceee866bcf5 Mon Sep 17 00:00:00 2001 From: Ilnur Date: Fri, 14 Mar 2025 16:42:56 +0400 Subject: [PATCH 06/15] add fireblocks usage into validators fns --- cmd/blockchaincmd/add_validator.go | 60 +++++++++++++++++++++------ cmd/blockchaincmd/remove_validator.go | 60 +++++++++++++++++++++------ 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/cmd/blockchaincmd/add_validator.go b/cmd/blockchaincmd/add_validator.go index df9fd9f18..46d8406e4 100644 --- a/cmd/blockchaincmd/add_validator.go +++ b/cmd/blockchaincmd/add_validator.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -199,19 +200,54 @@ func addValidator(cmd *cobra.Command, args []string) error { } // TODO: will estimate fee in subsecuent PR + var kc *keychain.Keychain fee := uint64(0) - kc, err := keychain.GetKeychainFromCmdLineFlags( - app, - "to pay for transaction fees on P-Chain", - network, - keyName, - useEwoq, - useLedger, - ledgerAddresses, - fee, - ) - if err != nil { - return err + if keyName == "fireblocks" { + fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") + if err != nil { + return err + } + + fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") + if err != nil { + return err + } + + fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") + if err != nil { + return err + } + + fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") + if err != nil { + return err + } + + fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") + if err != nil { + return err + } + + ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + if err != nil { + return err + } + + kc = keychain.NewKeychain(network, ckc, nil, nil) + } else { + kc, err = keychain.GetKeychainFromCmdLineFlags( + app, + "to pay for transaction fees on P-Chain", + network, + keyName, + useEwoq, + useLedger, + ledgerAddresses, + fee, + ) + if err != nil { + return err + } } sovereign := sc.Sovereign diff --git a/cmd/blockchaincmd/remove_validator.go b/cmd/blockchaincmd/remove_validator.go index 941c24873..3e22c29bb 100644 --- a/cmd/blockchaincmd/remove_validator.go +++ b/cmd/blockchaincmd/remove_validator.go @@ -8,6 +8,7 @@ import ( "os" "strings" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -98,19 +99,54 @@ func removeValidator(_ *cobra.Command, args []string) error { } // TODO: will estimate fee in subsecuent PR + var kc *keychain.Keychain fee := uint64(0) - kc, err := keychain.GetKeychainFromCmdLineFlags( - app, - "to pay for transaction fees on P-Chain", - network, - keyName, - useEwoq, - useLedger, - ledgerAddresses, - fee, - ) - if err != nil { - return err + if keyName == "fireblocks" { + fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") + if err != nil { + return err + } + + fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") + if err != nil { + return err + } + + fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") + if err != nil { + return err + } + + fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") + if err != nil { + return err + } + + fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") + if err != nil { + return err + } + + ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + if err != nil { + return err + } + + kc = keychain.NewKeychain(network, ckc, nil, nil) + } else { + kc, err = keychain.GetKeychainFromCmdLineFlags( + app, + "to pay for transaction fees on P-Chain", + network, + keyName, + useEwoq, + useLedger, + ledgerAddresses, + fee, + ) + if err != nil { + return err + } } network.HandlePublicNetworkSimulation() From 24408137bcbb1118bc93cef8db89ae502fcbc794 Mon Sep 17 00:00:00 2001 From: Ilnur Date: Fri, 14 Mar 2025 17:00:12 +0400 Subject: [PATCH 07/15] optimize function Addresses --- cmd/fireblockscmd/fireblocks/keychain.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/fireblockscmd/fireblocks/keychain.go b/cmd/fireblockscmd/fireblocks/keychain.go index 18b2709ff..7cca26742 100644 --- a/cmd/fireblockscmd/fireblocks/keychain.go +++ b/cmd/fireblockscmd/fireblocks/keychain.go @@ -54,9 +54,7 @@ func (fk *FireblocksKeychain) Get(addr ids.ShortID) (keychain.Signer, bool) { // Returns the set of addresses for which the accessor keeps an associated // signer func (fk *FireblocksKeychain) Addresses() set.Set[ids.ShortID] { - s := set.NewSet[ids.ShortID](1) - s.Add(fk.signer.Address()) - return s + return set.Of(fk.signer.Address()) } func NewFireblocksSigner(apiAddr, pk, ak, vaultid, assetid string) (*FireblocksSigner, error) { From 158891c60f76100e22f39a6532e8573ee7199dcd Mon Sep 17 00:00:00 2001 From: Ilnur Date: Fri, 14 Mar 2025 17:09:49 +0400 Subject: [PATCH 08/15] add address formatter --- cmd/fireblockscmd/addresscmd.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/cmd/fireblockscmd/addresscmd.go b/cmd/fireblockscmd/addresscmd.go index bbd22fc4e..36df5110b 100644 --- a/cmd/fireblockscmd/addresscmd.go +++ b/cmd/fireblockscmd/addresscmd.go @@ -5,22 +5,25 @@ import ( "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/spf13/cobra" ) var ( - apiAddr string - priKey string - apiKey string - vaultId string - assetId string + apiAddr string + priKey string + apiKey string + vaultId string + assetId string + chainAlias string + chainHrp string ) func newAddressCmd() *cobra.Command { cmd := &cobra.Command{ Use: "address", Short: "show fireblocks avalanche short id", - RunE: address, + RunE: addresscmd, Args: cobrautils.ExactArgs(0), Hidden: false, } @@ -29,14 +32,20 @@ func newAddressCmd() *cobra.Command { cmd.Flags().StringVar(&apiKey, "api-key", "e4fafe6f-742f-423c-b5fa-2af197e932d8", "fireblocks api key") cmd.Flags().StringVar(&vaultId, "vault-id", "219", "fireblocks vault id") cmd.Flags().StringVar(&assetId, "asset-id", "AVAXTEST", "fireblocks asset id") + cmd.Flags().StringVar(&chainAlias, "avalanche-chain-alias", "P", "avalanche network alias asset id") + cmd.Flags().StringVar(&chainHrp, "avalanche-chain-hrp", "fuji", "avalanche network hrp") return cmd } -func address(_ *cobra.Command, _ []string) error { +func addresscmd(_ *cobra.Command, _ []string) error { signer, err := fireblocks.NewFireblocksSigner(apiAddr, priKey, apiKey, vaultId, assetId) if err != nil { return err } - fmt.Printf("ShortID: %s\n", signer.Address()) + addr, err := address.Format(chainAlias, chainHrp, signer.Address().Bytes()) + if err != nil { + return err + } + fmt.Printf("Fireblocks address: %s\n", addr) return nil } From f949bf21233bda6b8c92b6913b42605e0d115abb Mon Sep 17 00:00:00 2001 From: Ilnur Date: Fri, 14 Mar 2025 21:23:57 +0400 Subject: [PATCH 09/15] add fireblocks keyname support for blockchain deployment --- cmd/blockchaincmd/deploy.go | 61 +++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index 7081612ed..32ae9bdef 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -14,6 +14,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/blockchain" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/cmd/interchaincmd/messengercmd" "github.com/ava-labs/avalanche-cli/cmd/interchaincmd/relayercmd" "github.com/ava-labs/avalanche-cli/cmd/networkcmd" @@ -555,18 +556,54 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { // createSubnet: add subnet fee fee := uint64(0) - kc, err := keychain.GetKeychainFromCmdLineFlags( - app, - constants.PayTxsFeesMsg, - network, - keyName, - useEwoq, - useLedger, - ledgerAddresses, - fee, - ) - if err != nil { - return err + var kc *keychain.Keychain + if keyName == "fireblocks" { + useLedger = false + fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") + if err != nil { + return err + } + + fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") + if err != nil { + return err + } + + fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") + if err != nil { + return err + } + + fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") + if err != nil { + return err + } + + fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") + if err != nil { + return err + } + + ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + if err != nil { + return err + } + + kc = keychain.NewKeychain(network, ckc, nil, nil) + } else { + kc, err = keychain.GetKeychainFromCmdLineFlags( + app, + constants.PayTxsFeesMsg, + network, + keyName, + useEwoq, + useLedger, + ledgerAddresses, + fee, + ) + if err != nil { + return err + } } availableBalance, err := utils.GetNetworkBalance(kc.Addresses().List(), network.Endpoint) From 8e39c556c0016d21885c63fd39abb1fed3b7ebe2 Mon Sep 17 00:00:00 2001 From: ramil Date: Sat, 15 Mar 2025 00:19:03 +0300 Subject: [PATCH 10/15] show fireblocks key --- pkg/utils/keys.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/utils/keys.go b/pkg/utils/keys.go index 3debae55c..f52989224 100644 --- a/pkg/utils/keys.go +++ b/pkg/utils/keys.go @@ -6,10 +6,11 @@ import ( "os" "strings" - "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -40,8 +41,8 @@ func GetKeyNames(keyDir string, addEwoq bool) ([]string, error) { } if addEwoq { userKeys = append(userKeys, "ewoq") - userKeys = append(userKeys, "fireblocks") } + userKeys = append(userKeys, "fireblocks") names = append(append(userKeys, subnetKeys...), cliKeys...) return names, nil } From 9be37c7a5f8911eacd7b1ca07453cb81f5fcb727 Mon Sep 17 00:00:00 2001 From: ramil Date: Sun, 16 Mar 2025 09:22:29 +0300 Subject: [PATCH 11/15] fireblocks: prompt, use HD Path for wallet generation --- cmd/blockchaincmd/add_validator.go | 48 ++++----------- cmd/blockchaincmd/deploy.go | 55 +++++------------ cmd/blockchaincmd/remove_validator.go | 40 +++---------- cmd/fireblockscmd/addresscmd.go | 22 ++++--- .../fireblocks/fireblocks_sdk.go | 20 ++++--- cmd/fireblockscmd/fireblocks/keychain.go | 25 ++++---- cmd/fireblockscmd/fireblocks/prompt.go | 60 +++++++++++++++++++ cmd/keycmd/transfer.go | 52 +++++----------- cmd/transactioncmd/transaction_sign.go | 32 ++-------- pkg/prompts/prompts.go | 18 +++--- 10 files changed, 161 insertions(+), 211 deletions(-) create mode 100644 cmd/fireblockscmd/fireblocks/prompt.go diff --git a/cmd/blockchaincmd/add_validator.go b/cmd/blockchaincmd/add_validator.go index 46d8406e4..f20e290b5 100644 --- a/cmd/blockchaincmd/add_validator.go +++ b/cmd/blockchaincmd/add_validator.go @@ -8,6 +8,16 @@ import ( "strings" "time" + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/ids" + avagoconstants "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/units" + warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -25,15 +35,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/validatormanager" sdkutils "github.com/ava-labs/avalanche-cli/sdk/utils" "github.com/ava-labs/avalanche-cli/sdk/validator" - "github.com/ava-labs/avalanchego/config" - "github.com/ava-labs/avalanchego/ids" - avagoconstants "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/formatting/address" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/units" - warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" - "github.com/ethereum/go-ethereum/common" - "github.com/spf13/cobra" ) var ( @@ -203,37 +204,12 @@ func addValidator(cmd *cobra.Command, args []string) error { var kc *keychain.Keychain fee := uint64(0) if keyName == "fireblocks" { - fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") - if err != nil { - return err - } - - fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") - if err != nil { - return err - } - - fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") - if err != nil { - return err - } - - fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") - if err != nil { - return err - } - - fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") - if err != nil { - return err - } - - ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) if err != nil { return err } - kc = keychain.NewKeychain(network, ckc, nil, nil) + kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) } else { kc, err = keychain.GetKeychainFromCmdLineFlags( app, diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index 32ae9bdef..2ce1104f5 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -14,6 +14,19 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/blockchain" + anrutils "github.com/ava-labs/avalanche-network-runner/utils" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + avagoutils "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/cmd/interchaincmd/messengercmd" "github.com/ava-labs/avalanche-cli/cmd/interchaincmd/relayercmd" @@ -32,18 +45,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanche-cli/pkg/vm" - anrutils "github.com/ava-labs/avalanche-network-runner/utils" - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/ids" - avagoutils "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/formatting/address" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/platformvm/fx" - "github.com/ava-labs/avalanchego/vms/platformvm/signer" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" @@ -558,38 +559,12 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { var kc *keychain.Keychain if keyName == "fireblocks" { - useLedger = false - fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") - if err != nil { - return err - } - - fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") - if err != nil { - return err - } - - fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") - if err != nil { - return err - } - - fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") - if err != nil { - return err - } - - fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") - if err != nil { - return err - } - - ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) if err != nil { return err } - kc = keychain.NewKeychain(network, ckc, nil, nil) + kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) } else { kc, err = keychain.GetKeychainFromCmdLineFlags( app, diff --git a/cmd/blockchaincmd/remove_validator.go b/cmd/blockchaincmd/remove_validator.go index 3e22c29bb..04b259158 100644 --- a/cmd/blockchaincmd/remove_validator.go +++ b/cmd/blockchaincmd/remove_validator.go @@ -8,6 +8,12 @@ import ( "os" "strings" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ethereum/go-ethereum/common" + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -25,11 +31,6 @@ import ( sdkutils "github.com/ava-labs/avalanche-cli/sdk/utils" validatorsdk "github.com/ava-labs/avalanche-cli/sdk/validator" validatormanagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) @@ -102,37 +103,12 @@ func removeValidator(_ *cobra.Command, args []string) error { var kc *keychain.Keychain fee := uint64(0) if keyName == "fireblocks" { - fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") - if err != nil { - return err - } - - fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") - if err != nil { - return err - } - - fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") - if err != nil { - return err - } - - fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") - if err != nil { - return err - } - - fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") - if err != nil { - return err - } - - ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) if err != nil { return err } - kc = keychain.NewKeychain(network, ckc, nil, nil) + kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) } else { kc, err = keychain.GetKeychainFromCmdLineFlags( app, diff --git a/cmd/fireblockscmd/addresscmd.go b/cmd/fireblockscmd/addresscmd.go index 36df5110b..0d8ba20b0 100644 --- a/cmd/fireblockscmd/addresscmd.go +++ b/cmd/fireblockscmd/addresscmd.go @@ -3,10 +3,11 @@ package fireblockscmd import ( "fmt" - "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" - "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/spf13/cobra" + + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" ) var ( @@ -28,24 +29,29 @@ func newAddressCmd() *cobra.Command { Hidden: false, } cmd.Flags().StringVar(&apiAddr, "api-addr", "https://sandbox-api.fireblocks.io", "fireblocks api address") - cmd.Flags().StringVar(&priKey, "private-key", "/Users/n0cte/Downloads/fireblocks_secret_editor_e4fafe6f-742f-423c-b5fa-2af197e932d8.key", "absolute path to fireblocks api private key") + cmd.Flags().StringVar(&priKey, "private-key", "/srv/landslide/fireblocks_secret_editor_e4fafe6f-742f-423c-b5fa-2af197e932d8.key", "absolute path to fireblocks api private key") cmd.Flags().StringVar(&apiKey, "api-key", "e4fafe6f-742f-423c-b5fa-2af197e932d8", "fireblocks api key") - cmd.Flags().StringVar(&vaultId, "vault-id", "219", "fireblocks vault id") - cmd.Flags().StringVar(&assetId, "asset-id", "AVAXTEST", "fireblocks asset id") + cmd.Flags().StringVar(&vaultId, "vault-id", "220", "fireblocks vault id") + cmd.Flags().StringVar(&assetId, "asset-id", "BTC_TEST", "fireblocks asset id") cmd.Flags().StringVar(&chainAlias, "avalanche-chain-alias", "P", "avalanche network alias asset id") cmd.Flags().StringVar(&chainHrp, "avalanche-chain-hrp", "fuji", "avalanche network hrp") return cmd } func addresscmd(_ *cobra.Command, _ []string) error { - signer, err := fireblocks.NewFireblocksSigner(apiAddr, priKey, apiKey, vaultId, assetId) + keychain, err := fireblocks.PromptFireblocks(app.Prompt) if err != nil { return err } - addr, err := address.Format(chainAlias, chainHrp, signer.Address().Bytes()) + addresses := keychain.Addresses() + addr, exists := addresses.Peek() + if !exists { + return fmt.Errorf("no address") + } + pChainAddress, err := address.Format(chainAlias, chainHrp, addr.Bytes()) if err != nil { return err } - fmt.Printf("Fireblocks address: %s\n", addr) + fmt.Printf("Fireblocks address: %s\n", pChainAddress) return nil } diff --git a/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go b/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go index f7e5c63a1..751e8a1cf 100644 --- a/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go +++ b/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go @@ -918,19 +918,16 @@ func (s *SDK) GetTransactionById(txId string) (TransactionDetails, error) { } -func (s *SDK) SignData(vaultid string, assetId string, data []byte) ([]byte, []byte, error) { +func (s *SDK) SignData(account, addressIndex int, data []byte) ([]byte, []byte, error) { req, err := json.Marshal(map[string]any{ - "source": map[string]any{ - "type": "VAULT_ACCOUNT", - "id": vaultid, - }, - "assetId": assetId, "operation": "RAW", "extraParameters": map[string]any{ "rawMessageData": map[string]any{ + "algorithm": "MPC_ECDSA_SECP256K1", "messages": []map[string]any{ { - "content": hex.EncodeToString(data), + "content": hex.EncodeToString(data), + "derivationPath": []int{44, 0, account, 0, addressIndex}, }, }, }, @@ -954,7 +951,8 @@ func (s *SDK) SignData(vaultid string, assetId string, data []byte) ([]byte, []b var ( receipt struct { - Status TransactionStatus `json:"status"` + Status TransactionStatus `json:"status"` + SubStatus TransactionSubStatus `json:"subStatus"` SignedMessages []struct { PublicKey string `json:"publicKey"` Signature struct { @@ -973,7 +971,11 @@ func (s *SDK) SignData(vaultid string, assetId string, data []byte) ([]byte, []b if err := json.Unmarshal([]byte(returnedData), &receipt); err != nil { return nil, nil, err } - complete = receipt.Status == "COMPLETED" || receipt.Status == "FAILED" + complete = receipt.Status == "COMPLETED" || receipt.Status == TransactionFailed + } + + if receipt.Status == TransactionFailed { + return nil, nil, fmt.Errorf("failed to sign transaction: %s", receipt.SubStatus) } if len(receipt.SignedMessages) != 1 { diff --git a/cmd/fireblockscmd/fireblocks/keychain.go b/cmd/fireblockscmd/fireblocks/keychain.go index 7cca26742..892bcbbe8 100644 --- a/cmd/fireblockscmd/fireblocks/keychain.go +++ b/cmd/fireblockscmd/fireblocks/keychain.go @@ -24,16 +24,17 @@ type FireblocksKeychain struct { } type FireblocksSigner struct { - sdk *SDK - vaultid string - assetid string + sdk *SDK + + account int + addressIndex int addr ids.ShortID mu sync.Mutex } -func NewFireblocksKeychain(apiAddr, pk, ak, vaultid, assetid string) (*FireblocksKeychain, error) { - signer, err := NewFireblocksSigner(apiAddr, pk, ak, vaultid, assetid) +func NewFireblocksKeychain(apiAddr, privateKeyPath, apiKey string, account, addressIndex int) (*FireblocksKeychain, error) { + signer, err := NewFireblocksSigner(apiAddr, privateKeyPath, apiKey, account, addressIndex) if err != nil { return nil, err } @@ -57,8 +58,8 @@ func (fk *FireblocksKeychain) Addresses() set.Set[ids.ShortID] { return set.Of(fk.signer.Address()) } -func NewFireblocksSigner(apiAddr, pk, ak, vaultid, assetid string) (*FireblocksSigner, error) { - f, err := os.Open(pk) +func NewFireblocksSigner(apiAddr, privateKeyPath, apiKey string, account, addressIndex int) (*FireblocksSigner, error) { + f, err := os.Open(privateKeyPath) if err != nil { return nil, err } @@ -70,9 +71,9 @@ func NewFireblocksSigner(apiAddr, pk, ak, vaultid, assetid string) (*FireblocksS } return &FireblocksSigner{ - sdk: NewInstance(pkBytes, ak, apiAddr, time.Hour), - vaultid: vaultid, - assetid: assetid, + sdk: NewInstance(pkBytes, apiKey, apiAddr, time.Hour), + account: account, + addressIndex: addressIndex, addr: ids.ShortEmpty, mu: sync.Mutex{}, @@ -80,7 +81,7 @@ func NewFireblocksSigner(apiAddr, pk, ak, vaultid, assetid string) (*FireblocksS } func (fs *FireblocksSigner) SignHash(hash []byte) ([]byte, error) { - sig, _, err := fs.sdk.SignData(fs.vaultid, fs.assetid, hash) + sig, _, err := fs.sdk.SignData(fs.account, fs.addressIndex, hash) return sig, err } @@ -98,7 +99,7 @@ func (fs *FireblocksSigner) Address() ids.ShortID { panic(err) } - _, rawpb, err := fs.sdk.SignData(fs.vaultid, fs.assetid, msg) + _, rawpb, err := fs.sdk.SignData(fs.account, fs.addressIndex, msg) if err != nil { panic(err) } diff --git a/cmd/fireblockscmd/fireblocks/prompt.go b/cmd/fireblockscmd/fireblocks/prompt.go new file mode 100644 index 000000000..cf8db7354 --- /dev/null +++ b/cmd/fireblockscmd/fireblocks/prompt.go @@ -0,0 +1,60 @@ +package fireblocks + +import ( + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/prompts" +) + +func PromptFireblocks(prompt prompts.Prompter) (*FireblocksKeychain, error) { + useSandbox, err := chooseSandboxOrProd(prompt) + if err != nil { + return nil, err + } + + privateKeyPath, err := prompt.CaptureString("Fireblocks private key path") + if err != nil { + return nil, err + } + privateKeyPath = strings.TrimSpace(privateKeyPath) + + apiKey, err := prompt.CaptureString("Fireblocks api key") + if err != nil { + return nil, err + } + + account, err := prompt.CaptureInt("Fireblocks bip44 account", func(n int) error { + return nil + }) + + addressIndex, err := prompt.CaptureInt("Fireblocks bip44 address index", func(n int) error { + return nil + }) + + var apiEndpoint string + if useSandbox { + apiEndpoint = "https://sandbox-api.fireblocks.io" + } else { + apiEndpoint = "https://api.fireblocks.io" + } + + fireblocksKc, err := NewFireblocksKeychain(apiEndpoint, privateKeyPath, apiKey, account, addressIndex) + if err != nil { + return nil, err + } + + return fireblocksKc, nil +} + +// chooseSandboxOrProd returns true if Sandbox environment is selected +func chooseSandboxOrProd(prompt prompts.Prompter) (bool, error) { + const ( + sandboxOption = "Sandbox" + prodOption = "Production" + ) + option, err := prompt.CaptureList("What Fireblocks environment should be used?", []string{sandboxOption, prodOption}) + if err != nil { + return false, err + } + return option == sandboxOption, nil +} diff --git a/cmd/keycmd/transfer.go b/cmd/keycmd/transfer.go index d2892e49f..397ef541e 100644 --- a/cmd/keycmd/transfer.go +++ b/cmd/keycmd/transfer.go @@ -8,18 +8,6 @@ import ( "math/big" "time" - "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" - "github.com/ava-labs/avalanche-cli/pkg/cobrautils" - "github.com/ava-labs/avalanche-cli/pkg/contract" - clievm "github.com/ava-labs/avalanche-cli/pkg/evm" - "github.com/ava-labs/avalanche-cli/pkg/ictt" - "github.com/ava-labs/avalanche-cli/pkg/key" - "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/networkoptions" - "github.com/ava-labs/avalanche-cli/pkg/prompts" - "github.com/ava-labs/avalanche-cli/pkg/utils" - "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanche-cli/pkg/vm" "github.com/ava-labs/avalanchego/ids" avagoconstants "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/keychain" @@ -36,6 +24,19 @@ import ( "github.com/ava-labs/coreth/plugin/evm/atomic" goethereumcommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" + + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanche-cli/pkg/contract" + clievm "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/ictt" + "github.com/ava-labs/avalanche-cli/pkg/key" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/networkoptions" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/pkg/vm" ) const ( @@ -246,32 +247,7 @@ func transferF(*cobra.Command, []string) error { var sk *key.SoftKey if keyName != "" { if keyName == "fireblocks" { - fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") - if err != nil { - return err - } - - fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") - if err != nil { - return err - } - - fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") - if err != nil { - return err - } - - fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") - if err != nil { - return err - } - - fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") - if err != nil { - return err - } - - kc, err = fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + kc, err = fireblocks.PromptFireblocks(app.Prompt) if err != nil { return err } diff --git a/cmd/transactioncmd/transaction_sign.go b/cmd/transactioncmd/transaction_sign.go index 64cea9964..67abdb3a2 100644 --- a/cmd/transactioncmd/transaction_sign.go +++ b/cmd/transactioncmd/transaction_sign.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -16,7 +18,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/subnet" "github.com/ava-labs/avalanche-cli/pkg/txutils" "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/ids" "github.com/spf13/cobra" ) @@ -152,37 +153,12 @@ func signTx(_ *cobra.Command, args []string) error { // get keychain accessor var kc *keychain.Keychain if keyName == "fireblocks" { - fireblocksApiAddr, err := app.Prompt.CaptureString("Press enter fireblocks api address") - if err != nil { - return err - } - - fireblocksPk, err := app.Prompt.CaptureString("Press enter absolute destination path to fireblocks key") - if err != nil { - return err - } - - fireblocksAk, err := app.Prompt.CaptureString("Press enter fireblocks api key") - if err != nil { - return err - } - - fireblocksVaultId, err := app.Prompt.CaptureString("Press enter vault id") - if err != nil { - return err - } - - fireblocksAssetId, err := app.Prompt.CaptureString("Press enter asset id") - if err != nil { - return err - } - - ckc, err := fireblocks.NewFireblocksKeychain(fireblocksApiAddr, fireblocksPk, fireblocksAk, fireblocksVaultId, fireblocksAssetId) + fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) if err != nil { return err } - kc = keychain.NewKeychain(network, ckc, nil, nil) + kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) } else { kc, err = keychain.GetKeychain(app, false, useLedger, ledgerAddresses, keyName, network, 0) if err != nil { diff --git a/pkg/prompts/prompts.go b/pkg/prompts/prompts.go index 21dd69bf2..30a2a4757 100644 --- a/pkg/prompts/prompts.go +++ b/pkg/prompts/prompts.go @@ -11,17 +11,18 @@ import ( "strings" "time" - "github.com/ava-labs/avalanche-cli/pkg/constants" - "github.com/ava-labs/avalanche-cli/pkg/key" - "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/utils" - "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanchego/ids" "github.com/ethereum/go-ethereum/common" "github.com/manifoldco/promptui" "github.com/spf13/cobra" "golang.org/x/exp/slices" "golang.org/x/mod/semver" + + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/key" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" ) type AddressFormat int64 @@ -864,12 +865,13 @@ func (*realPrompter) CaptureFutureDate(promptStr string, minDate time.Time) (tim // returns true [resp. false] if user chooses stored key [resp. ledger] option func (prompter *realPrompter) ChooseKeyOrLedger(goal string) (bool, error) { const ( - keyOption = "Use stored key" - ledgerOption = "Use ledger" + keyOption = "Use stored key" + ledgerOption = "Use ledger" + fireblocksOption = "Use fireblocks" ) option, err := prompter.CaptureList( fmt.Sprintf("Which key should be used %s?", goal), - []string{keyOption, ledgerOption}, + []string{keyOption, ledgerOption, fireblocksOption}, ) if err != nil { return false, err From dcd865039ef75abfc7dd380c179421b26cd169c0 Mon Sep 17 00:00:00 2001 From: ramil Date: Tue, 18 Mar 2025 20:31:06 +0300 Subject: [PATCH 12/15] fireblocks keychain refactoring --- cmd/blockchaincmd/add_validator.go | 35 +++--- cmd/blockchaincmd/change_owner.go | 4 +- cmd/blockchaincmd/change_weight.go | 14 +-- cmd/blockchaincmd/convert.go | 14 +-- cmd/blockchaincmd/deploy.go | 39 +++---- cmd/blockchaincmd/remove_validator.go | 36 +++---- cmd/fireblockscmd/addresscmd.go | 8 +- cmd/fireblockscmd/fireblocks/keychain.go | 13 ++- cmd/fireblockscmd/fireblocks/prompt.go | 60 ----------- cmd/keycmd/transfer.go | 38 ++++--- cmd/nodecmd/local.go | 20 ++-- cmd/nodecmd/validate_primary.go | 18 ++-- cmd/nodecmd/validate_subnet.go | 10 +- cmd/primarycmd/add_validator.go | 20 +++- cmd/transactioncmd/transaction_sign.go | 34 +++--- cmd/validatorcmd/increaseBalance.go | 9 +- pkg/keychain/keychain.go | 58 +++++++--- pkg/prompts/prompts.go | 131 ++++++++++++++++++++--- 18 files changed, 329 insertions(+), 232 deletions(-) delete mode 100644 cmd/fireblockscmd/fireblocks/prompt.go diff --git a/cmd/blockchaincmd/add_validator.go b/cmd/blockchaincmd/add_validator.go index f20e290b5..32cfe2020 100644 --- a/cmd/blockchaincmd/add_validator.go +++ b/cmd/blockchaincmd/add_validator.go @@ -18,7 +18,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" - "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -203,27 +202,19 @@ func addValidator(cmd *cobra.Command, args []string) error { // TODO: will estimate fee in subsecuent PR var kc *keychain.Keychain fee := uint64(0) - if keyName == "fireblocks" { - fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) - if err != nil { - return err - } - - kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) - } else { - kc, err = keychain.GetKeychainFromCmdLineFlags( - app, - "to pay for transaction fees on P-Chain", - network, - keyName, - useEwoq, - useLedger, - ledgerAddresses, - fee, - ) - if err != nil { - return err - } + kc, err = keychain.GetKeychainFromCmdLineFlags( + app, + "to pay for transaction fees on P-Chain", + network, + keyName, + useEwoq, + useLedger, + useFireblocks, + ledgerAddresses, + fee, + ) + if err != nil { + return err } sovereign := sc.Sovereign diff --git a/cmd/blockchaincmd/change_owner.go b/cmd/blockchaincmd/change_owner.go index 69db63eab..2425681ab 100644 --- a/cmd/blockchaincmd/change_owner.go +++ b/cmd/blockchaincmd/change_owner.go @@ -5,6 +5,8 @@ package blockchaincmd import ( "fmt" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/keychain" "github.com/ava-labs/avalanche-cli/pkg/networkoptions" @@ -13,7 +15,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/txutils" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/ids" "github.com/spf13/cobra" ) @@ -65,6 +66,7 @@ func changeOwner(_ *cobra.Command, args []string) error { keyName, useEwoq, useLedger, + useFireblocks, ledgerAddresses, fee, ) diff --git a/cmd/blockchaincmd/change_weight.go b/cmd/blockchaincmd/change_weight.go index be8a7b9b7..febe08374 100644 --- a/cmd/blockchaincmd/change_weight.go +++ b/cmd/blockchaincmd/change_weight.go @@ -5,6 +5,13 @@ package blockchaincmd import ( "fmt" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/spf13/cobra" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/contract" @@ -17,12 +24,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanche-cli/sdk/validator" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/formatting" - "github.com/ava-labs/avalanchego/utils/formatting/address" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/spf13/cobra" ) var newWeight uint64 @@ -81,6 +82,7 @@ func setWeight(_ *cobra.Command, args []string) error { keyName, useEwoq, useLedger, + useFireblocks, ledgerAddresses, fee, ) diff --git a/cmd/blockchaincmd/convert.go b/cmd/blockchaincmd/convert.go index 085af091e..fb3e3bdf1 100644 --- a/cmd/blockchaincmd/convert.go +++ b/cmd/blockchaincmd/convert.go @@ -10,6 +10,13 @@ import ( "strings" "time" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -30,12 +37,6 @@ import ( blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" sdkutils "github.com/ava-labs/avalanche-cli/sdk/utils" validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/config" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" @@ -619,6 +620,7 @@ func convertBlockchain(_ *cobra.Command, args []string) error { keyName, useEwoq, useLedger, + useFireblocks, ledgerAddresses, fee, ) diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index 2ce1104f5..afee009b8 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -27,7 +27,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" - "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/cmd/interchaincmd/messengercmd" "github.com/ava-labs/avalanche-cli/cmd/interchaincmd/relayercmd" "github.com/ava-labs/avalanche-cli/cmd/networkcmd" @@ -61,6 +60,7 @@ var ( userProvidedAvagoVersion string outputTxPath string useLedger bool + useFireblocks bool useLocalMachine bool useEwoq bool ledgerAddresses []string @@ -136,6 +136,7 @@ so you can take your locally tested Blockchain and deploy it on Fuji or Mainnet. cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "file path of the blockchain creation tx") cmd.Flags().BoolVarP(&useEwoq, "ewoq", "e", false, "use ewoq key [fuji/devnet deploy only]") cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on fuji/devnet)") + cmd.Flags().BoolVar(&useFireblocks, "fireblocks", false, "use Fireblocks instead of key") cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses") cmd.Flags().StringVarP(&subnetIDStr, "subnet-id", "u", "", "do not create a subnet, deploy the blockchain into the given subnet id") cmd.Flags().Uint32Var(&mainnetChainID, "mainnet-chain-id", 0, "use different ChainID for mainnet deployment") @@ -556,29 +557,19 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { // !subnetonly: add blockchain fee // createSubnet: add subnet fee fee := uint64(0) - - var kc *keychain.Keychain - if keyName == "fireblocks" { - fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) - if err != nil { - return err - } - - kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) - } else { - kc, err = keychain.GetKeychainFromCmdLineFlags( - app, - constants.PayTxsFeesMsg, - network, - keyName, - useEwoq, - useLedger, - ledgerAddresses, - fee, - ) - if err != nil { - return err - } + kc, err := keychain.GetKeychainFromCmdLineFlags( + app, + constants.PayTxsFeesMsg, + network, + keyName, + useEwoq, + useLedger, + useFireblocks, + ledgerAddresses, + fee, + ) + if err != nil { + return err } availableBalance, err := utils.GetNetworkBalance(kc.Addresses().List(), network.Endpoint) diff --git a/cmd/blockchaincmd/remove_validator.go b/cmd/blockchaincmd/remove_validator.go index 04b259158..172018f1d 100644 --- a/cmd/blockchaincmd/remove_validator.go +++ b/cmd/blockchaincmd/remove_validator.go @@ -14,7 +14,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ethereum/go-ethereum/common" - "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -100,29 +99,20 @@ func removeValidator(_ *cobra.Command, args []string) error { } // TODO: will estimate fee in subsecuent PR - var kc *keychain.Keychain fee := uint64(0) - if keyName == "fireblocks" { - fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) - if err != nil { - return err - } - - kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) - } else { - kc, err = keychain.GetKeychainFromCmdLineFlags( - app, - "to pay for transaction fees on P-Chain", - network, - keyName, - useEwoq, - useLedger, - ledgerAddresses, - fee, - ) - if err != nil { - return err - } + kc, err := keychain.GetKeychainFromCmdLineFlags( + app, + "to pay for transaction fees on P-Chain", + network, + keyName, + useEwoq, + useLedger, + useFireblocks, + ledgerAddresses, + fee, + ) + if err != nil { + return err } network.HandlePublicNetworkSimulation() diff --git a/cmd/fireblockscmd/addresscmd.go b/cmd/fireblockscmd/addresscmd.go index 0d8ba20b0..491cdf86d 100644 --- a/cmd/fireblockscmd/addresscmd.go +++ b/cmd/fireblockscmd/addresscmd.go @@ -8,6 +8,7 @@ import ( "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanche-cli/pkg/prompts" ) var ( @@ -39,7 +40,11 @@ func newAddressCmd() *cobra.Command { } func addresscmd(_ *cobra.Command, _ []string) error { - keychain, err := fireblocks.PromptFireblocks(app.Prompt) + fbParams, err := prompts.PromptFireblocks(app.Prompt) + if err != nil { + return err + } + keychain, err := fireblocks.NewFireblocksKeychain(fbParams) if err != nil { return err } @@ -48,6 +53,7 @@ func addresscmd(_ *cobra.Command, _ []string) error { if !exists { return fmt.Errorf("no address") } + fmt.Printf("ShortID: %s\n", addr) pChainAddress, err := address.Format(chainAlias, chainHrp, addr.Bytes()) if err != nil { return err diff --git a/cmd/fireblockscmd/fireblocks/keychain.go b/cmd/fireblockscmd/fireblocks/keychain.go index 892bcbbe8..978cb5fbe 100644 --- a/cmd/fireblockscmd/fireblocks/keychain.go +++ b/cmd/fireblockscmd/fireblocks/keychain.go @@ -12,6 +12,8 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/set" + + "github.com/ava-labs/avalanche-cli/pkg/prompts" ) var ( @@ -33,8 +35,15 @@ type FireblocksSigner struct { mu sync.Mutex } -func NewFireblocksKeychain(apiAddr, privateKeyPath, apiKey string, account, addressIndex int) (*FireblocksKeychain, error) { - signer, err := NewFireblocksSigner(apiAddr, privateKeyPath, apiKey, account, addressIndex) +func NewFireblocksKeychain(params *prompts.FireblocksParams) (keychain.Keychain, error) { + var apiEndpoint string + if params.UseSandbox { + apiEndpoint = "https://sandbox-api.fireblocks.io" + } else { + apiEndpoint = "https://api.fireblocks.io" + } + + signer, err := NewFireblocksSigner(apiEndpoint, params.PrivateKeyPath, params.APIKey, params.Account, params.AddressIndex) if err != nil { return nil, err } diff --git a/cmd/fireblockscmd/fireblocks/prompt.go b/cmd/fireblockscmd/fireblocks/prompt.go deleted file mode 100644 index cf8db7354..000000000 --- a/cmd/fireblockscmd/fireblocks/prompt.go +++ /dev/null @@ -1,60 +0,0 @@ -package fireblocks - -import ( - "strings" - - "github.com/ava-labs/avalanche-cli/pkg/prompts" -) - -func PromptFireblocks(prompt prompts.Prompter) (*FireblocksKeychain, error) { - useSandbox, err := chooseSandboxOrProd(prompt) - if err != nil { - return nil, err - } - - privateKeyPath, err := prompt.CaptureString("Fireblocks private key path") - if err != nil { - return nil, err - } - privateKeyPath = strings.TrimSpace(privateKeyPath) - - apiKey, err := prompt.CaptureString("Fireblocks api key") - if err != nil { - return nil, err - } - - account, err := prompt.CaptureInt("Fireblocks bip44 account", func(n int) error { - return nil - }) - - addressIndex, err := prompt.CaptureInt("Fireblocks bip44 address index", func(n int) error { - return nil - }) - - var apiEndpoint string - if useSandbox { - apiEndpoint = "https://sandbox-api.fireblocks.io" - } else { - apiEndpoint = "https://api.fireblocks.io" - } - - fireblocksKc, err := NewFireblocksKeychain(apiEndpoint, privateKeyPath, apiKey, account, addressIndex) - if err != nil { - return nil, err - } - - return fireblocksKc, nil -} - -// chooseSandboxOrProd returns true if Sandbox environment is selected -func chooseSandboxOrProd(prompt prompts.Prompter) (bool, error) { - const ( - sandboxOption = "Sandbox" - prodOption = "Production" - ) - option, err := prompt.CaptureList("What Fireblocks environment should be used?", []string{sandboxOption, prodOption}) - if err != nil { - return false, err - } - return option == sandboxOption, nil -} diff --git a/cmd/keycmd/transfer.go b/cmd/keycmd/transfer.go index 397ef541e..de1bd7480 100644 --- a/cmd/keycmd/transfer.go +++ b/cmd/keycmd/transfer.go @@ -218,6 +218,9 @@ func transferF(*cobra.Command, []string) error { return fmt.Errorf("transfer from %s to %s is not supported", senderDesc, receiverDesc) } + var keyType prompts.KeyType + var fb *prompts.FireblocksParams + if keyName == "" && ledgerIndex == wrongLedgerIndexVal { var useLedger bool goalStr := "as the sender address" @@ -231,10 +234,13 @@ func transferF(*cobra.Command, []string) error { ux.Logger.PrintToUser("Tokens will be transferred to the same account address on the other chain") goalStr = "as the sender/receiver address" } - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, goalStr, app.GetKeyDir(), true) + keyType, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, goalStr, app.GetKeyDir(), true) if err != nil { return err } + if keyType == prompts.Ledger { + useLedger = true + } if useLedger { ledgerIndex, err = app.Prompt.CaptureUint32("Ledger index to use") if err != nil { @@ -245,28 +251,28 @@ func transferF(*cobra.Command, []string) error { var kc keychain.Keychain var sk *key.SoftKey - if keyName != "" { - if keyName == "fireblocks" { - kc, err = fireblocks.PromptFireblocks(app.Prompt) + if keyType == prompts.StoredKey { + sk, err = app.GetKey(keyName, network, false) + if err != nil { + return err + } + kc = sk.KeyChain() + } else { + if keyType == prompts.Fireblocks { + kc, err = fireblocks.NewFireblocksKeychain(fb) if err != nil { return err } } else { - sk, err = app.GetKey(keyName, network, false) + ledgerDevice, err := ledger.New() + if err != nil { + return err + } + ledgerIndices := []uint32{ledgerIndex} + kc, err = keychain.NewLedgerKeychainFromIndices(ledgerDevice, ledgerIndices) if err != nil { return err } - kc = sk.KeyChain() - } - } else { - ledgerDevice, err := ledger.New() - if err != nil { - return err - } - ledgerIndices := []uint32{ledgerIndex} - kc, err = keychain.NewLedgerKeychainFromIndices(ledgerDevice, ledgerIndices) - if err != nil { - return err } } usingLedger := ledgerIndex != wrongLedgerIndexVal diff --git a/cmd/nodecmd/local.go b/cmd/nodecmd/local.go index 9bed7f0c5..3a23f907b 100644 --- a/cmd/nodecmd/local.go +++ b/cmd/nodecmd/local.go @@ -8,6 +8,16 @@ import ( "strings" "time" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/api" + warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanche-cli/pkg/binutils" "github.com/ava-labs/avalanche-cli/pkg/blockchain" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -23,15 +33,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanche-cli/pkg/validatormanager" sdkutils "github.com/ava-labs/avalanche-cli/sdk/utils" - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/config" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/formatting/address" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/platformvm" - "github.com/ava-labs/avalanchego/vms/platformvm/api" - warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/spf13/cobra" ) @@ -329,6 +330,7 @@ func localValidate(_ *cobra.Command, args []string) error { keyName, useEwoq, useLedger, + useFireblocks, ledgerAddresses, fee, ) diff --git a/cmd/nodecmd/validate_primary.go b/cmd/nodecmd/validate_primary.go index 26fb1b3dc..e8636bf43 100644 --- a/cmd/nodecmd/validate_primary.go +++ b/cmd/nodecmd/validate_primary.go @@ -11,6 +11,14 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/spf13/cobra" + "golang.org/x/exp/maps" + blockchaincmd "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -20,19 +28,13 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/subnet" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/platformvm" - "github.com/ava-labs/avalanchego/vms/platformvm/signer" - "github.com/spf13/cobra" - "golang.org/x/exp/maps" ) var ( keyName string useEwoq bool useLedger bool + useFireblocks bool useStaticIP bool awsProfile string ledgerAddresses []string @@ -59,6 +61,7 @@ Network.`, cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji only]") cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on fuji/devnet)") + cmd.Flags().BoolVar(&useFireblocks, "fireblocks", false, "use Fireblocks instead of key") cmd.Flags().BoolVarP(&useEwoq, "ewoq", "e", false, "use ewoq key [fuji/devnet only]") cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses") @@ -317,6 +320,7 @@ func validatePrimaryNetwork(_ *cobra.Command, args []string) error { keyName, useEwoq, useLedger, + useFireblocks, ledgerAddresses, fee, ) diff --git a/cmd/nodecmd/validate_subnet.go b/cmd/nodecmd/validate_subnet.go index f006709ef..27e7d8ce7 100644 --- a/cmd/nodecmd/validate_subnet.go +++ b/cmd/nodecmd/validate_subnet.go @@ -10,6 +10,11 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/platformvm/status" + "github.com/spf13/cobra" + "golang.org/x/exp/maps" + blockchaincmd "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -19,10 +24,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/ssh" "github.com/ava-labs/avalanche-cli/pkg/subnet" "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/vms/platformvm/status" - "github.com/spf13/cobra" - "golang.org/x/exp/maps" ) var avoidSubnetValidationChecks bool @@ -226,6 +227,7 @@ func validateSubnet(_ *cobra.Command, args []string) error { keyName, useEwoq, useLedger, + useFireblocks, ledgerAddresses, fee, ) diff --git a/cmd/primarycmd/add_validator.go b/cmd/primarycmd/add_validator.go index 8273dda3e..fe335cbcd 100644 --- a/cmd/primarycmd/add_validator.go +++ b/cmd/primarycmd/add_validator.go @@ -9,6 +9,9 @@ import ( "math" "time" + "github.com/ava-labs/avalanchego/ids" + "github.com/spf13/cobra" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/cmd/nodecmd" "github.com/ava-labs/avalanche-cli/pkg/application" @@ -20,14 +23,13 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/prompts" "github.com/ava-labs/avalanche-cli/pkg/subnet" "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/ids" - "github.com/spf13/cobra" ) var ( globalNetworkFlags networkoptions.NetworkFlags keyName string useLedger bool + useFireblocks bool ledgerAddresses []string nodeIDStr string weight uint64 @@ -62,6 +64,7 @@ in the Primary Network`, cmd.Flags().StringVar(&startTimeStr, "start-time", "", "UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format") cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long this validator will be staking") cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on fuji)") + cmd.Flags().BoolVar(&useFireblocks, "fireblocks", false, "use Fireblocks") cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses") cmd.Flags().StringVar(&publicKey, "public-key", "", "set the BLS public key of the validator to add") cmd.Flags().StringVar(&pop, "proof-of-possession", "", "set the BLS proof of possession of the validator to add") @@ -135,13 +138,22 @@ func addValidator(_ *cobra.Command, _ []string) error { return ErrMutuallyExlusiveKeyLedger } + var keyType prompts.KeyType + var fb *prompts.FireblocksParams + switch network.Kind { case models.Fuji: if !useLedger && keyName == "" { - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, constants.PayTxsFeesMsg, app.GetKeyDir(), false) + keyType, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, constants.PayTxsFeesMsg, app.GetKeyDir(), false) if err != nil { return err } + if keyType == prompts.Ledger { + useLedger = true + } + if keyType == prompts.Fireblocks { + useFireblocks = true + } } case models.Mainnet: useLedger = true @@ -180,7 +192,7 @@ func addValidator(_ *cobra.Command, _ []string) error { // TODO: will estimate fee in subsecuent PR fee := uint64(0) - kc, err := keychain.GetKeychain(app, false, useLedger, ledgerAddresses, keyName, network, fee) + kc, err := keychain.GetKeychain(app, false, useLedger, useFireblocks, fb, ledgerAddresses, keyName, network, fee) if err != nil { return err } diff --git a/cmd/transactioncmd/transaction_sign.go b/cmd/transactioncmd/transaction_sign.go index 67abdb3a2..8b230664b 100644 --- a/cmd/transactioncmd/transaction_sign.go +++ b/cmd/transactioncmd/transaction_sign.go @@ -9,7 +9,6 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" - "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/keychain" @@ -28,6 +27,7 @@ var ( inputTxPath string keyName string useLedger bool + useFireblocks bool ledgerAddresses []string ) @@ -44,6 +44,7 @@ func newTransactionSignCmd() *cobra.Command { cmd.Flags().StringVar(&inputTxPath, inputTxPathFlag, "", "Path to the transaction file for signing") cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji only]") cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on fuji)") + cmd.Flags().BoolVar(&useFireblocks, "fireblocks", false, "use Fireblocks") cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses") return cmd } @@ -80,6 +81,9 @@ func signTx(_ *cobra.Command, args []string) error { return blockchaincmd.ErrMutuallyExlusiveKeyLedger } + var keyType prompts.KeyType + var fb *prompts.FireblocksParams + // we need network to decide if ledger is forced (mainnet) network, err := txutils.GetNetwork(tx) if err != nil { @@ -88,26 +92,32 @@ func signTx(_ *cobra.Command, args []string) error { switch network.Kind { case models.Local: if !useLedger && keyName == "" { - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, "sign transaction", app.GetKeyDir(), true) + keyType, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, "sign transaction", app.GetKeyDir(), true) if err != nil { return err } } case models.Fuji: if !useLedger && keyName == "" { - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, "sign transaction", app.GetKeyDir(), false) + keyType, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, "sign transaction", app.GetKeyDir(), false) if err != nil { return err } } case models.Mainnet: - useLedger = keyName != "fireblocks" + useLedger = true if keyName != "" { return blockchaincmd.ErrStoredKeyOnMainnet } default: return errors.New("unsupported network") } + if keyType == prompts.Ledger { + useLedger = true + } + if keyType == prompts.Fireblocks { + useFireblocks = true + } // we need subnet ID for the wallet signing validation + process subnetID, err := txutils.GetSubnetID(tx) @@ -151,19 +161,9 @@ func signTx(_ *cobra.Command, args []string) error { } // get keychain accessor - var kc *keychain.Keychain - if keyName == "fireblocks" { - fireblocksKeychain, err := fireblocks.PromptFireblocks(app.Prompt) - if err != nil { - return err - } - - kc = keychain.NewKeychain(network, fireblocksKeychain, nil, nil) - } else { - kc, err = keychain.GetKeychain(app, false, useLedger, ledgerAddresses, keyName, network, 0) - if err != nil { - return err - } + kc, err := keychain.GetKeychain(app, false, useLedger, useFireblocks, fb, ledgerAddresses, keyName, network, 0) + if err != nil { + return err } // add control keys to the keychain whenever possible diff --git a/cmd/validatorcmd/increaseBalance.go b/cmd/validatorcmd/increaseBalance.go index 0766a8705..62f70464f 100644 --- a/cmd/validatorcmd/increaseBalance.go +++ b/cmd/validatorcmd/increaseBalance.go @@ -7,6 +7,10 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/blockchain" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/spf13/cobra" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/keychain" @@ -15,14 +19,12 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanche-cli/sdk/validator" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/spf13/cobra" ) var ( keyName string useLedger bool + useFireblocks bool useEwoq bool ledgerAddresses []string balanceAVAX float64 @@ -80,6 +82,7 @@ func increaseBalance(_ *cobra.Command, _ []string) error { keyName, useEwoq, useLedger, + useFireblocks, ledgerAddresses, fee, ) diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index 6c4e262c6..97210e475 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -6,13 +6,6 @@ import ( "errors" "fmt" - "github.com/ava-labs/avalanche-cli/cmd/flags" - "github.com/ava-labs/avalanche-cli/pkg/application" - "github.com/ava-labs/avalanche-cli/pkg/key" - "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/prompts" - "github.com/ava-labs/avalanche-cli/pkg/utils" - "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/keychain" "github.com/ava-labs/avalanchego/utils/crypto/ledger" @@ -21,6 +14,15 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/platformvm" + + "github.com/ava-labs/avalanche-cli/cmd/fireblockscmd/fireblocks" + "github.com/ava-labs/avalanche-cli/cmd/flags" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/key" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" ) const ( @@ -113,6 +115,7 @@ func GetKeychainFromCmdLineFlags( keyName string, useEwoq bool, useLedger bool, + useFireblocks bool, ledgerAddresses []string, requiredFunds uint64, ) (*Keychain, error) { @@ -124,24 +127,38 @@ func GetKeychainFromCmdLineFlags( if !flags.EnsureMutuallyExclusive([]bool{useLedger, useEwoq, keyName != ""}) { return nil, ErrMutuallyExlusiveKeySource } + var keyType prompts.KeyType + var fb *prompts.FireblocksParams switch { case network.Kind == models.Local: // prompt the user if no key source was provided if !useEwoq && !useLedger && keyName == "" { var err error - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), true) + keyType, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), true) if err != nil { return nil, err } + if keyType == prompts.Ledger { + useLedger = true + } + if keyType == prompts.Fireblocks { + useFireblocks = true + } } case network.Kind == models.Devnet: // prompt the user if no key source was provided if !useEwoq && !useLedger && keyName == "" { var err error - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), true) + keyType, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), true) if err != nil { return nil, err } + if keyType == prompts.Ledger { + useLedger = true + } + if keyType == prompts.Fireblocks { + useFireblocks = true + } } case network.Kind == models.Fuji: if useEwoq { @@ -150,29 +167,38 @@ func GetKeychainFromCmdLineFlags( // prompt the user if no key source was provided if !useLedger && keyName == "" { var err error - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), false) + keyType, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), false) if err != nil { return nil, err } + if keyType == prompts.Fireblocks { + useFireblocks = true + } } case network.Kind == models.Mainnet: // mainnet requires ledger usage if keyName != "" || useEwoq { return nil, ErrStoredKeyOrEwoqOnMainnet } - useLedger = true + var err error + _, keyName, fb, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), false) + if err != nil { + return nil, err + } } network.HandlePublicNetworkSimulation() // get keychain accessor - return GetKeychain(app, useEwoq, useLedger, ledgerAddresses, keyName, network, requiredFunds) + return GetKeychain(app, useEwoq, useLedger, useFireblocks, fb, ledgerAddresses, keyName, network, requiredFunds) } func GetKeychain( app *application.Avalanche, useEwoq bool, useLedger bool, + useFireblocks bool, + fbParams *prompts.FireblocksParams, ledgerAddresses []string, keyName string, network models.Network, @@ -216,6 +242,14 @@ func GetKeychain( } return NewKeychain(network, kc, ledgerDevice, ledgerIndices), nil } + if useFireblocks { + fbKeychain, err := fireblocks.NewFireblocksKeychain(fbParams) + if err != nil { + return nil, err + } + + return NewKeychain(network, fbKeychain, nil, nil), nil + } if useEwoq { sf, err := app.GetKey("ewoq", network, false) if err != nil { diff --git a/pkg/prompts/prompts.go b/pkg/prompts/prompts.go index 30a2a4757..95959f8f5 100644 --- a/pkg/prompts/prompts.go +++ b/pkg/prompts/prompts.go @@ -27,6 +27,8 @@ import ( type AddressFormat int64 +type KeyType int64 + const ( Undefined AddressFormat = iota PChainFormat @@ -34,6 +36,21 @@ const ( XChainFormat ) +const ( + UndefinedKeyType KeyType = iota + StoredKey + Ledger + Fireblocks +) + +type FireblocksParams struct { + UseSandbox bool + PrivateKeyPath string + APIKey string + Account int + AddressIndex int +} + const ( Yes = "Yes" No = "No" @@ -122,7 +139,7 @@ type Prompter interface { CapturePChainAddress(promptStr string, network models.Network) (string, error) CaptureXChainAddress(promptStr string, network models.Network) (string, error) CaptureFutureDate(promptStr string, minDate time.Time) (time.Time, error) - ChooseKeyOrLedger(goal string) (bool, error) + ChooseKeyOrLedger(goal string) (KeyType, error) } type realPrompter struct{} @@ -863,7 +880,7 @@ func (*realPrompter) CaptureFutureDate(promptStr string, minDate time.Time) (tim } // returns true [resp. false] if user chooses stored key [resp. ledger] option -func (prompter *realPrompter) ChooseKeyOrLedger(goal string) (bool, error) { +func (prompter *realPrompter) ChooseKeyOrLedger(goal string) (KeyType, error) { const ( keyOption = "Use stored key" ledgerOption = "Use ledger" @@ -874,9 +891,18 @@ func (prompter *realPrompter) ChooseKeyOrLedger(goal string) (bool, error) { []string{keyOption, ledgerOption, fireblocksOption}, ) if err != nil { - return false, err + return UndefinedKeyType, err + } + switch option { + case keyOption: + return StoredKey, nil + case ledgerOption: + return Ledger, nil + case fireblocksOption: + return Fireblocks, nil } - return option == keyOption, nil + + return UndefinedKeyType, fmt.Errorf("Unknown key type %s", option) } func contains[T comparable](list []T, element T) bool { @@ -953,22 +979,36 @@ func GetSubnetAuthKeys(prompt Prompter, walletKeys []string, controlKeys []strin return subnetAuthKeys, nil } -func GetKeyOrLedger(prompt Prompter, goal string, keyDir string, includeEwoq bool) (bool, string, error) { - useStoredKey, err := prompt.ChooseKeyOrLedger(goal) +func GetKeyOrLedger(prompt Prompter, goal string, keyDir string, includeEwoq bool) (KeyType, string, *FireblocksParams, error) { + keyType, err := prompt.ChooseKeyOrLedger(goal) if err != nil { - return false, "", err + return UndefinedKeyType, "", nil, err } - if !useStoredKey { - return true, "", nil + if keyType == Ledger { + return keyType, "", nil, nil } - keyName, err := CaptureKeyName(prompt, goal, keyDir, includeEwoq) - if err != nil { - if errors.Is(err, errNoKeys) { - ux.Logger.PrintToUser("No private keys have been found. Create a new one with `avalanche key create`") + + if keyType == StoredKey { + keyName, err := CaptureKeyName(prompt, goal, keyDir, includeEwoq) + if err != nil { + if errors.Is(err, errNoKeys) { + ux.Logger.PrintToUser("No private keys have been found. Create a new one with `avalanche key create`") + } + return UndefinedKeyType, "", nil, err + } + return StoredKey, keyName, nil, nil + } + + if keyType == Fireblocks { + fireblocksParams, err := PromptFireblocks(prompt) + if err != nil { + return UndefinedKeyType, "", nil, err } - return false, "", err + + return Fireblocks, "", fireblocksParams, nil } - return false, keyName, nil + + return UndefinedKeyType, "", nil, fmt.Errorf("unknown key type %d", keyType) } func CaptureKeyName(prompt Prompter, goal string, keyDir string, includeEwoq bool) (string, error) { @@ -1205,3 +1245,64 @@ func CaptureKeyAddress( } return "", nil } + +func PromptFireblocks(prompt Prompter) (*FireblocksParams, error) { + useSandbox, err := chooseSandboxOrProd(prompt) + if err != nil { + return nil, err + } + + privateKeyPath, err := prompt.CaptureString("Fireblocks private key path") + if err != nil { + return nil, err + } + privateKeyPath = strings.TrimSpace(privateKeyPath) + + apiKey, err := prompt.CaptureString("Fireblocks api key") + if err != nil { + return nil, err + } + + account, err := prompt.CaptureInt("Fireblocks bip44 account", func(n int) error { + return nil + }) + + addressIndex, err := prompt.CaptureInt("Fireblocks bip44 address index", func(n int) error { + return nil + }) + + return &FireblocksParams{ + UseSandbox: useSandbox, + PrivateKeyPath: privateKeyPath, + APIKey: apiKey, + Account: account, + AddressIndex: addressIndex, + }, nil + + //var apiEndpoint string + //if useSandbox { + // apiEndpoint = "https://sandbox-api.fireblocks.io" + //} else { + // apiEndpoint = "https://api.fireblocks.io" + //} + // + //fireblocksKc, err := NewFireblocksKeychain(apiEndpoint, privateKeyPath, apiKey, account, addressIndex) + //if err != nil { + // return nil, err + //} + // + //return fireblocksKc, nil +} + +// chooseSandboxOrProd returns true if Sandbox environment is selected +func chooseSandboxOrProd(prompt Prompter) (bool, error) { + const ( + sandboxOption = "Sandbox" + prodOption = "Production" + ) + option, err := prompt.CaptureList("What Fireblocks environment should be used?", []string{sandboxOption, prodOption}) + if err != nil { + return false, err + } + return option == sandboxOption, nil +} From fcef5a51f0c25f8aedb2c66441b83cb8fe2387fb Mon Sep 17 00:00:00 2001 From: ramil Date: Wed, 19 Mar 2025 12:03:03 +0300 Subject: [PATCH 13/15] fireblocks: fix signature gathering --- cmd/fireblockscmd/fireblocks/fireblocks_sdk.go | 14 +++++++++++--- cmd/fireblockscmd/fireblocks/keychain.go | 16 ++++++++++++++-- pkg/utils/keys.go | 1 - 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go b/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go index 751e8a1cf..421508cf7 100644 --- a/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go +++ b/cmd/fireblockscmd/fireblocks/fireblocks_sdk.go @@ -16,6 +16,7 @@ import ( "math/rand" "net/http" "net/url" + "strconv" "strings" "time" @@ -927,7 +928,7 @@ func (s *SDK) SignData(account, addressIndex int, data []byte) ([]byte, []byte, "messages": []map[string]any{ { "content": hex.EncodeToString(data), - "derivationPath": []int{44, 0, account, 0, addressIndex}, + "derivationPath": []int{44, 9000, account, 0, addressIndex}, }, }, }, @@ -956,6 +957,9 @@ func (s *SDK) SignData(account, addressIndex int, data []byte) ([]byte, []byte, SignedMessages []struct { PublicKey string `json:"publicKey"` Signature struct { + R string `json:"r"` + S string `json:"s"` + V int `json:"v"` FullSig string `json:"fullSig"` } } `json:"signedMessages"` @@ -982,12 +986,16 @@ func (s *SDK) SignData(account, addressIndex int, data []byte) ([]byte, []byte, return nil, nil, fmt.Errorf("signatures not found") } - rawPublicKey, err := hex.DecodeString(receipt.SignedMessages[0].PublicKey) + signedMessage := receipt.SignedMessages[0] + + rawPublicKey, err := hex.DecodeString(signedMessage.PublicKey) if err != nil { return nil, nil, err } - rawSignature, err := hex.DecodeString(receipt.SignedMessages[0].Signature.FullSig) + sig := signedMessage.Signature + + rawSignature, err := hex.DecodeString(sig.R + sig.S + "0" + strconv.Itoa(sig.V)) if err != nil { return nil, nil, err } diff --git a/cmd/fireblockscmd/fireblocks/keychain.go b/cmd/fireblockscmd/fireblocks/keychain.go index 978cb5fbe..6840aed39 100644 --- a/cmd/fireblockscmd/fireblocks/keychain.go +++ b/cmd/fireblockscmd/fireblocks/keychain.go @@ -2,6 +2,7 @@ package fireblocks import ( "encoding/hex" + "fmt" "io" "os" "sync" @@ -108,17 +109,28 @@ func (fs *FireblocksSigner) Address() ids.ShortID { panic(err) } - _, rawpb, err := fs.sdk.SignData(fs.account, fs.addressIndex, msg) + rawSignature, rawPublicKey, err := fs.sdk.SignData(fs.account, fs.addressIndex, msg) if err != nil { panic(err) } - pb, err := secp256k1.ToPublicKey(rawpb) + pb, err := secp256k1.ToPublicKey(rawPublicKey) if err != nil { panic(err) } fs.addr = pb.Address() + fmt.Printf("PB1 ShortID %s\n", fs.addr) + + pb2, err := secp256k1.RecoverPublicKeyFromHash(msg, rawSignature) + if err != nil { + panic(err) + } + pb2b := pb2.Bytes() + fmt.Printf("PB2 public key %s\n", hex.EncodeToString(pb2b)) + + pb2Addr := pb2.Address() + fmt.Printf("PB2 ShortID %s\n", pb2Addr) } return fs.addr diff --git a/pkg/utils/keys.go b/pkg/utils/keys.go index f52989224..04971f44f 100644 --- a/pkg/utils/keys.go +++ b/pkg/utils/keys.go @@ -42,7 +42,6 @@ func GetKeyNames(keyDir string, addEwoq bool) ([]string, error) { if addEwoq { userKeys = append(userKeys, "ewoq") } - userKeys = append(userKeys, "fireblocks") names = append(append(userKeys, subnetKeys...), cliKeys...) return names, nil } From 0359337a1de847618d058baafe5f822837c4765c Mon Sep 17 00:00:00 2001 From: ramil Date: Tue, 1 Apr 2025 17:57:27 +0300 Subject: [PATCH 14/15] fireblocks upd --- cmd/blockchaincmd/deploy.go | 2 +- pkg/keychain/keychain.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index afee009b8..73dd6ed9c 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -666,7 +666,7 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { if createSubnet { if sidecar.Sovereign { - sameControlKey = true + sameControlKey = false } controlKeys, threshold, err = promptOwners( kc, diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index 97210e475..723edd5a2 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -204,7 +204,7 @@ func GetKeychain( network models.Network, requiredFunds uint64, ) (*Keychain, error) { - if !useEwoq && !useLedger && keyName == "" { + if !useEwoq && !useLedger && !useFireblocks && keyName == "" { return nil, fmt.Errorf("one of the options ewoq/ledger/keyName must be provided") } // get keychain accessor From 6840fe855f6502a62400bb7bd30d05b36920dec9 Mon Sep 17 00:00:00 2001 From: ramil Date: Wed, 2 Apr 2025 11:55:04 +0300 Subject: [PATCH 15/15] update text for fireblocks --- pkg/keychain/keychain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index 723edd5a2..66d3a66f6 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -205,7 +205,7 @@ func GetKeychain( requiredFunds uint64, ) (*Keychain, error) { if !useEwoq && !useLedger && !useFireblocks && keyName == "" { - return nil, fmt.Errorf("one of the options ewoq/ledger/keyName must be provided") + return nil, fmt.Errorf("one of the options ewoq/ledger/fireblocks/keyName must be provided") } // get keychain accessor if useLedger {