Skip to content

Commit

Permalink
Add support for transaction notes
Browse files Browse the repository at this point in the history
Extended SendAsset and related functions to include an optional note field for each transaction. Updated go.mod dependencies and adjusted send configurations accordingly.
  • Loading branch information
pbennett committed Oct 10, 2024
1 parent 865fb0e commit 22d93c5
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 44 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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.π
Expand Down
6 changes: 4 additions & 2 deletions assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)

Expand Down
12 changes: 4 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -38,25 +36,23 @@ 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=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
15 changes: 13 additions & 2 deletions nfd-helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,24 @@ 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
)

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)
}
Expand All @@ -221,6 +230,7 @@ func getAssetSendTxns(sender string, sendFromVaultName string, recipient string,
Receiver: recipient,
ReceiverType: receiverType,
Sender: sender, // owner address
Note: note,
},
sendFromVaultName,
)
Expand All @@ -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,
)
Expand Down
45 changes: 21 additions & 24 deletions sendconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 15 additions & 4 deletions sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down

0 comments on commit 22d93c5

Please sign in to comment.