From 22d93c56bc23d0f79590309f1322f6c032633b55 Mon Sep 17 00:00:00 2001 From: Patrick Bennett Date: Wed, 9 Oct 2024 22:57:44 -0400 Subject: [PATCH] Add support for transaction notes Extended SendAsset and related functions to include an optional note field for each transaction. Updated go.mod dependencies and adjusted send configurations accordingly. --- README.md | 6 ++++-- assets.go | 6 ++++-- go.mod | 4 ++-- go.sum | 12 ++++-------- main.go | 1 + nfd-helpers.go | 15 +++++++++++++-- sendconfiguration.go | 45 +++++++++++++++++++++----------------------- sender.go | 19 +++++++++++++++---- 8 files changed, 64 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 445fc29..312affd 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,8 @@ Some of these options conflict with eachother if specified together. "asset": { "asa": 123456, "amount": 1000000, - "isPerRecip": false + "isPerRecip": false, + "note": "optional note with each transaction" } }, "destination": { @@ -112,10 +113,11 @@ Some of these options conflict with eachother if specified together. } ``` -**Send**: This will likely change significiantly in the future, but right now this just lists the single asset to send (a fungible token). +**Send**: This will likely change significantly in the future, but right now this just lists the single asset to send (a fungible token). - `asa`: The id of the asset to send. - `amount`: The amount of asset to send. This is in the denominated units of the Asset, not its base units. ie: Assume sending ALGO then 1.5 here really means 1,500,000 microAlgo. - `isPerRecip`: Determines whether the amount is per recipient or the total amount to send. If amount is 100 and isPerRecip is not set or false, then 100 is divided across all recipients. If isPerRecip is set, then it would be 100 per recipient. +- `note`: An optional note to include with the transaction **Destination**: This configures the recipients of the assets. - `csvFile`: Path to CSV file to load NFD names from (makes some options irrelevant). The first row must contain column names - with the column containing the nfd name named either nfd or name.π diff --git a/assets.go b/assets.go index 0513047..e2995de 100644 --- a/assets.go +++ b/assets.go @@ -13,16 +13,18 @@ type SendAsset struct { ExistingBalance uint64 AmountToSend float64 IsAmountPerRecip bool + Note string } // write String method for SendAsset func (a *SendAsset) String() string { - return fmt.Sprintf("AssetID: %d (%s), ExistingBalance: %s, AmountToSend: %f, IsAmountPerRecip: %t", + return fmt.Sprintf("AssetID: %d (%s), ExistingBalance: %s, AmountToSend: %f, IsAmountPerRecip: %t, Note:%s", a.AssetID, a.AssetParams.UnitName, a.formattedAmount(a.ExistingBalance), a.AmountToSend, - a.IsAmountPerRecip) + a.IsAmountPerRecip, + a.Note) } func (s *SendAsset) formattedAmount(amount uint64) string { diff --git a/go.mod b/go.mod index b011833..a3913f8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/TxnLab/batch-asset-send -go 1.22 +go 1.23 require ( github.com/algorand/go-algorand-sdk/v2 v2.6.0 @@ -9,7 +9,7 @@ require ( github.com/mailgun/holster/v4 v4.20.3 github.com/ssgreg/repeat v1.5.1 golang.org/x/crypto v0.28.0 - golang.org/x/oauth2 v0.21.0 + golang.org/x/oauth2 v0.23.0 golang.org/x/time v0.7.0 ) diff --git a/go.sum b/go.sum index 420725a..4f9a607 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,6 @@ github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASu github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/mailgun/holster/v4 v4.20.0 h1:K8KCpyaim+yFbjcUQ5q4TcRXZhIWJxwOUwjlh9FPAuE= -github.com/mailgun/holster/v4 v4.20.0/go.mod h1:/5ijRCyMjOHxt69WdAgvB2gyYCapJaJdT/QciGIcu50= github.com/mailgun/holster/v4 v4.20.3 h1:FwHxBvuoWEqEpZGeNCLuk/oAHyNs3+ksGoCW0qbiHyo= github.com/mailgun/holster/v4 v4.20.3/go.mod h1:HuFVoS8qOhceEBL4czXnVzp0bQrrIkLeX30IAll5hQ0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -38,16 +36,14 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -55,8 +51,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/main.go b/main.go index 248056e..57710ef 100644 --- a/main.go +++ b/main.go @@ -148,6 +148,7 @@ func fetchAssets(config *BatchSendConfig) ([]*SendAsset, error) { ExistingBalance: holdingInfo.AssetHolding.Amount, AmountToSend: config.Send.Asset.Amount, IsAmountPerRecip: config.Send.Asset.IsPerRecip, + Note: config.Send.Asset.Note, }) return assetsToSend, nil } diff --git a/nfd-helpers.go b/nfd-helpers.go index d45d244..af48194 100644 --- a/nfd-helpers.go +++ b/nfd-helpers.go @@ -191,7 +191,16 @@ func getAllSegments(ctx context.Context, config *BatchSendConfig, parentAppID in return nfds, nil } -func getAssetSendTxns(sender string, sendFromVaultName string, recipient string, recipientIsVault bool, assetID uint64, amount uint64, params types.SuggestedParams) (string, []byte, error) { +func getAssetSendTxns( + sender string, + sendFromVaultName string, + recipient string, + recipientIsVault bool, + assetID uint64, + amount uint64, + note string, + params types.SuggestedParams, +) (string, []byte, error) { var ( encodedTxns string err error @@ -199,7 +208,7 @@ func getAssetSendTxns(sender string, sendFromVaultName string, recipient string, if sendFromVaultName == "" && recipientIsVault == false { // Not sending from vault, nor sending to a vault - so just plain asset transfer - txn, err := transaction.MakeAssetTransferTxn(sender, recipient, amount, nil, params, "", assetID) + txn, err := transaction.MakeAssetTransferTxn(sender, recipient, amount, []byte(note), params, "", assetID) if err != nil { return "", nil, fmt.Errorf("MakeAssetTransferTxn fail: %w", err) } @@ -221,6 +230,7 @@ func getAssetSendTxns(sender string, sendFromVaultName string, recipient string, Receiver: recipient, ReceiverType: receiverType, Sender: sender, // owner address + Note: note, }, sendFromVaultName, ) @@ -232,6 +242,7 @@ func getAssetSendTxns(sender string, sendFromVaultName string, recipient string, Amount: int64(amount), Assets: []int64{int64(assetID)}, Sender: sender, // owner address + Note: note, }, recipient, ) diff --git a/sendconfiguration.go b/sendconfiguration.go index 44d62ff..5d0035d 100644 --- a/sendconfiguration.go +++ b/sendconfiguration.go @@ -8,6 +8,26 @@ import ( "strings" ) +type BatchSendConfig struct { + Send SendChoice `json:"send"` + + Destination DestinationChoice `json:"destination"` +} + +type SendChoice struct { + Asset struct { + ASA uint64 `json:"asa"` + // If IsPerRcp is NOT set then this is the TOTAL amount to send - and will be divided across destination + // count - if IsPerRcp is set then amount is amount per recipient + // Specified in user-friendly units - not base units - ie 1.5 ALGO would be 1.5, not 1,500,000 + Amount float64 `json:"amount"` + // Is the amount 'per recipient' or is it total amount to send. + IsPerRecip bool `json:"isPerRecip"` + // what note to include with the transaction + Note string `json:"note,omitempty"` + } `json:"asset"` +} + type DestinationChoice struct { // a csv file of recipient nfds to send to (if not opted in only nfd sends [to vaults] will work) CsvFile string `json:"csvfile"` @@ -56,34 +76,11 @@ func (dc DestinationChoice) String() string { sb.WriteString(fmt.Sprintf("Verified (v.*) requirements: %v, ", dc.VerifiedRequirements)) } if dc.SegmentsOfRoot == "" && !dc.OnlyRoots && dc.RandomNFDs.Count == 0 && len(dc.VerifiedRequirements) == 0 { - sb.WriteString("Sending to ALL owned NFDs") + sb.WriteString("Sending to ALL owned (matching) NFDs") } return sb.String() } -type AssetChoice struct { - // If specifying a 'list' of assets to send - if so, assumed '1' base unit per chosen recipient (ie: 1 nft) - //CSVFilename string `json:"csvFilename"` - - Asset struct { - ASA uint64 `json:"asa"` - // If IsPerRcp is NOT set then this is the TOTAL amount to send - and will be divided across destination - // count - if IsPerRcp is set then amount is amount per recipient - // Specified in user-friendly units - not base units - ie 1.5 ALGO would be 1.5, not 1,500,000 - Amount float64 `json:"amount"` - // Is the amount 'per recipient' or is it total amount to send. - IsPerRecip bool `json:"isPerRecip"` - } `json:"asset"` -} - -// 1.000000 ALGO -> 1,000,000 microAlgo - -type BatchSendConfig struct { - Send AssetChoice `json:"send"` - - Destination DestinationChoice `json:"destination"` -} - func loadJSONConfig(filename string) (*BatchSendConfig, error) { jsonFile, err := os.Open(filename) if err != nil { diff --git a/sender.go b/sender.go index dc08b61..c887cd4 100644 --- a/sender.go +++ b/sender.go @@ -61,6 +61,7 @@ type SendRequest struct { amount uint64 recipient Recipient sendFromVaultNFD *nfdapi.NfdRecord + note string } func sendAssets(sender string, send []*SendAsset, recipients []*Recipient, vaultNfd *nfdapi.NfdRecord, dryRun bool) { @@ -137,13 +138,13 @@ func appendToFile(message string, filename string) { } } -func QueueSends(sendRequests chan SendRequest, send []*SendAsset, sender string, recipients []*Recipient, sendFromVaultNFD *nfdapi.NfdRecord) { +func QueueSends(sendRequests chan SendRequest, sendAsset []*SendAsset, sender string, recipients []*Recipient, sendFromVaultNFD *nfdapi.NfdRecord) { var ( // Get new params every 30 secs or so txParams = algo.SuggestedParams(ctx, logger, algoClient) ticker = time.NewTicker(30 * time.Second) ) - for _, asset := range send { + for _, asset := range sendAsset { var amount float64 if asset.IsAmountPerRecip { amount = asset.AmountToSend @@ -179,7 +180,8 @@ func sendAssetToRecipient(sender string, sendReq *SendRequest, dryRun bool) *Rec retReceipt := &RecipientTransaction{ sendAsset: &sendReq.asset, baseUnitsToSend: sendReq.amount, - recip: &sendReq.recipient} + recip: &sendReq.recipient, + } // First, is this a send FROM a vault or from an account? if sendReq.sendFromVaultNFD != nil { @@ -200,7 +202,16 @@ func sendAssetToRecipient(sender string, sendReq *SendRequest, dryRun bool) *Rec return retReceipt } - txnId, signedBytes, err := getAssetSendTxns(sender, sendFromVaultName, recipAsString, sendReq.recipient.SendToVault, sendReq.asset.AssetID, sendReq.amount, sendReq.params) + txnId, signedBytes, err := getAssetSendTxns( + sender, + sendFromVaultName, + recipAsString, + sendReq.recipient.SendToVault, + sendReq.asset.AssetID, + sendReq.amount, + sendReq.asset.Note, + sendReq.params, + ) if err != nil { retReceipt.Error = fmt.Errorf("failure getting txns: %w", err) return retReceipt