Skip to content

Commit 3a01bc1

Browse files
authored
Merge pull request #2133 from onflow/cf/cancel-tx
Add schedule `cancel` and `list` logic
2 parents c8280cb + e676179 commit 3a01bc1

File tree

7 files changed

+392
-87
lines changed

7 files changed

+392
-87
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ install-tools:
3131

3232
.PHONY: test
3333
test:
34-
GO111MODULE=on go test -coverprofile=$(COVER_PROFILE) $(if $(JSON_OUTPUT),-json,) ./...
34+
CGO_ENABLED=1 CGO_CFLAGS="-O2 -D__BLST_PORTABLE__ -std=gnu11" GO111MODULE=on go test -coverprofile=$(COVER_PROFILE) $(if $(JSON_OUTPUT),-json,) ./...
3535

3636
.PHONY: test-e2e-emulator
3737
test-e2e-emulator:
@@ -53,7 +53,7 @@ ci: install-tools generate test coverage
5353

5454
$(BINARY):
5555
CGO_ENABLED=1 \
56-
CGO_CFLAGS="-O2 -D__BLST_PORTABLE__" \
56+
CGO_CFLAGS="-O2 -D__BLST_PORTABLE__ -std=gnu11" \
5757
GO111MODULE=on go build \
5858
-trimpath \
5959
-ldflags \

internal/schedule/cancel.go

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,20 @@
1919
package schedule
2020

2121
import (
22+
"context"
2223
"fmt"
24+
"strconv"
2325

26+
"github.com/onflow/cadence"
27+
flowsdk "github.com/onflow/flow-go-sdk"
2428
"github.com/spf13/cobra"
2529

2630
"github.com/onflow/flowkit/v2"
31+
"github.com/onflow/flowkit/v2/accounts"
2732
"github.com/onflow/flowkit/v2/output"
33+
"github.com/onflow/flowkit/v2/transactions"
2834

35+
"github.com/onflow/flow-cli/common/branding"
2936
"github.com/onflow/flow-cli/internal/command"
3037
"github.com/onflow/flow-cli/internal/util"
3138
)
@@ -71,42 +78,141 @@ func cancelRun(
7178
return nil, fmt.Errorf("transaction ID is required as an argument")
7279
}
7380

74-
transactionID := args[0]
81+
transactionIDStr := args[0]
82+
83+
// Parse transaction ID as UInt64
84+
transactionID, err := strconv.ParseUint(transactionIDStr, 10, 64)
85+
if err != nil {
86+
return nil, fmt.Errorf("invalid transaction ID: %w", err)
87+
}
7588

7689
signer, err := util.GetSignerAccount(state, cancelFlags.Signer)
7790
if err != nil {
7891
return nil, err
7992
}
8093

81-
logger.Info(fmt.Sprintf("Network: %s", globalFlags.Network))
82-
logger.Info(fmt.Sprintf("Signer: %s (%s)", cancelFlags.Signer, signer.Address.String()))
83-
logger.Info(fmt.Sprintf("Transaction ID: %s", transactionID))
94+
chainID, err := util.NetworkToChainID(globalFlags.Network)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
if chainID == flowsdk.Mainnet {
100+
return nil, fmt.Errorf("transaction scheduling is not yet supported on mainnet")
101+
}
102+
103+
schedulerUtilsAddress, err := getContractAddress(FlowTransactionSchedulerUtils, chainID)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
flowTokenAddress, err := getContractAddress(FlowToken, chainID)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
fungibleTokenAddress, err := getContractAddress(FungibleToken, chainID)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
networkStr := branding.GrayStyle.Render(globalFlags.Network)
119+
addressStr := branding.PurpleStyle.Render(signer.Address.HexWithPrefix())
120+
signerStr := branding.GrayStyle.Render(cancelFlags.Signer)
121+
txIDStr := branding.PurpleStyle.Render(transactionIDStr)
122+
84123
logger.Info("Canceling scheduled transaction...")
124+
logger.Info("")
125+
logger.Info(fmt.Sprintf("🌐 Network: %s", networkStr))
126+
logger.Info(fmt.Sprintf("📝 Signer: %s (%s)", signerStr, addressStr))
127+
logger.Info(fmt.Sprintf("🔍 Transaction ID: %s", txIDStr))
128+
logger.Info("")
129+
130+
// Build transaction code
131+
cancelTx := fmt.Sprintf(`import FlowTransactionSchedulerUtils from %s
132+
import FlowToken from %s
133+
import FungibleToken from %s
134+
135+
transaction(transactionId: UInt64) {
136+
let manager: auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}
137+
let tokenReceiver: &{FungibleToken.Receiver}
138+
139+
prepare(signer: auth(BorrowValue) &Account) {
140+
// 1. Borrow Manager reference
141+
self.manager = signer.storage.borrow<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(
142+
from: FlowTransactionSchedulerUtils.managerStoragePath
143+
) ?? panic("Could not borrow Manager. Please ensure you have a Manager set up.")
144+
145+
// Verify transaction exists in manager
146+
assert(
147+
self.manager.getTransactionIDs().contains(transactionId),
148+
message: "Transaction with ID ".concat(transactionId.toString()).concat(" not found in manager")
149+
)
150+
151+
// 2. Get FlowToken receiver to deposit refunds
152+
self.tokenReceiver = signer.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
153+
.borrow()
154+
?? panic("Could not borrow FlowToken receiver")
155+
}
156+
157+
execute {
158+
// Cancel the transaction and receive refunded fees
159+
let refundVault <- self.manager.cancel(id: transactionId)
160+
161+
// Deposit refunded fees back to the account
162+
self.tokenReceiver.deposit(from: <-refundVault)
163+
}
164+
}`, schedulerUtilsAddress, flowTokenAddress, fungibleTokenAddress)
165+
166+
_, txResult, err := flow.SendTransaction(
167+
context.Background(),
168+
transactions.AccountRoles{
169+
Proposer: *signer,
170+
Authorizers: []accounts.Account{*signer},
171+
Payer: *signer,
172+
},
173+
flowkit.Script{
174+
Code: []byte(cancelTx),
175+
Args: []cadence.Value{cadence.NewUInt64(transactionID)},
176+
},
177+
1000,
178+
)
179+
180+
if err != nil {
181+
return nil, fmt.Errorf("failed to cancel scheduled transaction: %w", err)
182+
}
183+
184+
if txResult.Error != nil {
185+
return nil, fmt.Errorf("cancel transaction failed: %s", txResult.Error.Error())
186+
}
85187

86-
// TODO: Implement cancel logic for scheduled transaction
188+
logger.Info("")
189+
successIcon := branding.GreenStyle.Render("✅")
190+
successMsg := branding.GreenStyle.Render("Scheduled transaction canceled successfully")
191+
logger.Info(fmt.Sprintf("%s %s", successIcon, successMsg))
87192

88193
return &cancelResult{
89-
success: true,
90-
message: fmt.Sprintf("Scheduled transaction %s canceled successfully", transactionID),
194+
success: true,
195+
transactionID: transactionIDStr,
91196
}, nil
92197
}
93198

94199
type cancelResult struct {
95-
success bool
96-
message string
200+
success bool
201+
transactionID string
97202
}
98203

99204
func (r *cancelResult) JSON() any {
100205
return map[string]any{
101-
"success": r.success,
102-
"message": r.message,
206+
"success": r.success,
207+
"transactionID": r.transactionID,
208+
"message": "Scheduled transaction canceled successfully",
103209
}
104210
}
105211

106212
func (r *cancelResult) String() string {
107-
return r.message
213+
return ""
108214
}
109215

110216
func (r *cancelResult) Oneliner() string {
111-
return r.message
217+
return fmt.Sprintf("Scheduled transaction %s canceled successfully", r.transactionID)
112218
}

internal/schedule/contracts.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@ import (
2222
"fmt"
2323

2424
flowsdk "github.com/onflow/flow-go-sdk"
25+
"github.com/onflow/flow-go/fvm/systemcontracts"
26+
flowGo "github.com/onflow/flow-go/model/flow"
2527
)
2628

2729
// ContractName represents a scheduler-related contract name
2830
type ContractName string
2931

3032
const (
31-
// FlowTransactionSchedulerUtils contract provides utility functions for transaction scheduling
3233
FlowTransactionSchedulerUtils ContractName = "FlowTransactionSchedulerUtils"
33-
// FlowTransactionScheduler contract handles the core transaction scheduling logic
34-
FlowTransactionScheduler ContractName = "FlowTransactionScheduler"
34+
FlowTransactionScheduler ContractName = "FlowTransactionScheduler"
35+
FlowToken ContractName = "FlowToken"
36+
FungibleToken ContractName = "FungibleToken"
3537
)
3638

3739
// contractAddresses maps contract names to their addresses on different networks
@@ -48,12 +50,34 @@ var contractAddresses = map[ContractName]map[flowsdk.ChainID]string{
4850

4951
// getContractAddress returns the contract address for the given contract name and network
5052
func getContractAddress(contract ContractName, chainID flowsdk.ChainID) (string, error) {
51-
// Check if mainnet
52-
if chainID == flowsdk.Mainnet {
53+
// Check if mainnet for scheduling contracts
54+
if chainID == flowsdk.Mainnet && (contract == FlowTransactionSchedulerUtils || contract == FlowTransactionScheduler) {
5355
return "", fmt.Errorf("transaction scheduling is not yet supported on mainnet")
5456
}
5557

56-
// Look up the contract address
58+
// Handle system contracts using the systemcontracts library
59+
if contract == FlowToken || contract == FungibleToken {
60+
var flowGoChainID flowGo.ChainID
61+
switch chainID {
62+
case flowsdk.Emulator:
63+
flowGoChainID = flowGo.Emulator
64+
case flowsdk.Testnet:
65+
flowGoChainID = flowGo.Testnet
66+
case flowsdk.Mainnet:
67+
flowGoChainID = flowGo.Mainnet
68+
default:
69+
return "", fmt.Errorf("unsupported chain ID: %s", chainID)
70+
}
71+
72+
systemContracts := systemcontracts.SystemContractsForChain(flowGoChainID)
73+
switch contract {
74+
case FlowToken:
75+
return systemContracts.FlowToken.Address.HexWithPrefix(), nil
76+
case FungibleToken:
77+
return systemContracts.FungibleToken.Address.HexWithPrefix(), nil
78+
}
79+
}
80+
5781
networkAddresses, contractExists := contractAddresses[contract]
5882
if !contractExists {
5983
return "", fmt.Errorf("unknown contract: %s", contract)

internal/schedule/get.go

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func getRun(
9797
networkStr := branding.GrayStyle.Render(globalFlags.Network)
9898
txIDStr := branding.PurpleStyle.Render(transactionIDStr)
9999

100-
logger.Info("Getting scheduled transaction details")
100+
logger.Info("Getting scheduled transaction details...")
101101
logger.Info("")
102102
logger.Info(fmt.Sprintf("🌐 Network: %s", networkStr))
103103
logger.Info(fmt.Sprintf("🔍 Transaction ID: %s", txIDStr))
@@ -131,7 +131,6 @@ access(all) fun main(transactionID: UInt64): FlowTransactionScheduler.Transactio
131131
return nil, fmt.Errorf("scheduled transaction not found")
132132
}
133133

134-
// Log success
135134
logger.Info("")
136135
successIcon := branding.GreenStyle.Render("✅")
137136
successMsg := branding.GreenStyle.Render("Transaction data retrieved successfully")
@@ -158,49 +157,7 @@ func (r *getResult) JSON() any {
158157
}
159158

160159
func (r *getResult) String() string {
161-
var output string
162-
163-
// ID
164-
idLabel := branding.GrayStyle.Render(" ID:")
165-
idValue := branding.PurpleStyle.Render(fmt.Sprintf("%d", r.data.ID))
166-
output += fmt.Sprintf("%s %s\n", idLabel, idValue)
167-
168-
// Status
169-
statusLabel := branding.GrayStyle.Render(" Status:")
170-
statusValue := branding.GreenStyle.Render(GetStatusString(r.data.Status))
171-
output += fmt.Sprintf("%s %s\n", statusLabel, statusValue)
172-
173-
// Priority
174-
priorityLabel := branding.GrayStyle.Render(" Priority:")
175-
priorityValue := branding.PurpleStyle.Render(GetPriorityString(r.data.Priority))
176-
output += fmt.Sprintf("%s %s\n", priorityLabel, priorityValue)
177-
178-
// Execution Effort
179-
effortLabel := branding.GrayStyle.Render(" Execution Effort:")
180-
effortValue := branding.PurpleStyle.Render(fmt.Sprintf("%d", r.data.ExecutionEffort))
181-
output += fmt.Sprintf("%s %s\n", effortLabel, effortValue)
182-
183-
// Fees
184-
feesLabel := branding.GrayStyle.Render(" Fees:")
185-
feesValue := branding.PurpleStyle.Render(fmt.Sprintf("%s FLOW", r.data.Fees))
186-
output += fmt.Sprintf("%s %s\n", feesLabel, feesValue)
187-
188-
// Scheduled Timestamp
189-
timestampLabel := branding.GrayStyle.Render(" Scheduled Timestamp:")
190-
timestampValue := branding.PurpleStyle.Render(r.data.ScheduledTimestamp)
191-
output += fmt.Sprintf("%s %s\n", timestampLabel, timestampValue)
192-
193-
// Handler Type
194-
handlerTypeLabel := branding.GrayStyle.Render(" Handler Type:")
195-
handlerTypeValue := branding.PurpleStyle.Render(r.data.HandlerTypeIdentifier)
196-
output += fmt.Sprintf("%s %s\n", handlerTypeLabel, handlerTypeValue)
197-
198-
// Handler Address
199-
handlerAddrLabel := branding.GrayStyle.Render(" Handler Address:")
200-
handlerAddrValue := branding.PurpleStyle.Render(r.data.HandlerAddress)
201-
output += fmt.Sprintf("%s %s\n", handlerAddrLabel, handlerAddrValue)
202-
203-
return output
160+
return FormatTransactionDetails(r.data)
204161
}
205162

206163
func (r *getResult) Oneliner() string {

0 commit comments

Comments
 (0)