Skip to content

Commit 2bcd63d

Browse files
committed
add COA handler, schedule tx, and simple test
1 parent b333d07 commit 2bcd63d

File tree

6 files changed

+402
-1
lines changed

6 files changed

+402
-1
lines changed

contracts/FlowTransactionSchedulerUtils.cdc

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import "FlowTransactionScheduler"
2+
import "FungibleToken"
23
import "FlowToken"
4+
import "EVM"
5+
import "MetadataViews"
36

47
/// FlowTransactionSchedulerUtils provides utility functionality for working with scheduled transactions
58
/// on the Flow blockchain. Currently, it only includes a Manager resource for managing scheduled transactions.
@@ -561,6 +564,174 @@ access(all) contract FlowTransactionSchedulerUtils {
561564
return getAccount(at).capabilities.borrow<&{Manager}>(self.managerPublicPath)
562565
}
563566

567+
/*********************************************
568+
569+
COA Handler Utils
570+
571+
**********************************************/
572+
573+
access(all) view fun coaHandlerStoragePath(): StoragePath {
574+
return /storage/coaScheduledTransactionHandler
575+
}
576+
577+
access(all) view fun coaHandlerPublicPath(): PublicPath {
578+
return /public/coaScheduledTransactionHandler
579+
}
580+
581+
/// COATransactionHandler is a resource that wraps a capability to a COA (Contract Owned Account)
582+
/// and implements the TransactionHandler interface to allow scheduling transactions for COAs.
583+
/// This handler enables users to schedule transactions that will be executed on behalf of their COA.
584+
access(all) resource COATransactionHandler: FlowTransactionScheduler.TransactionHandler {
585+
/// The capability to the COA resource
586+
access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
587+
588+
/// The capability to the FlowToken vault
589+
access(self) let flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
590+
591+
init(coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
592+
flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
593+
)
594+
{
595+
self.coaCapability = coaCapability
596+
self.flowTokenVaultCapability = flowTokenVaultCapability
597+
}
598+
599+
/// Execute the scheduled transaction using the COA
600+
/// @param id: The ID of the scheduled transaction
601+
/// @param data: Optional data passed to the transaction execution. In this case, the data needs to be a COAHandlerParams struct with valid values.
602+
access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
603+
let coa = self.coaCapability.borrow()
604+
?? panic("COA capability is invalid or expired for scheduled transaction with ID \(id)")
605+
606+
if let params = data as? COAHandlerParams {
607+
switch params.txType {
608+
case COAHandlerTxType.DepositFLOW:
609+
if params.amount == nil {
610+
panic("Amount is required for deposit for scheduled transaction with ID \(id)")
611+
}
612+
let vault = self.flowTokenVaultCapability.borrow()
613+
?? panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id)")
614+
coa.deposit(from: <-vault.withdraw(amount: params.amount!) as! @FlowToken.Vault)
615+
case COAHandlerTxType.WithdrawFLOW:
616+
if params.amount == nil {
617+
panic("Amount is required for withdrawal from COA for scheduled transaction with ID \(id)")
618+
}
619+
let vault = self.flowTokenVaultCapability.borrow()
620+
?? panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id)")
621+
let amount = EVM.Balance(attoflow: 0)
622+
amount.setFLOW(flow: params.amount!)
623+
vault.deposit(from: <-coa.withdraw(balance: amount) as! @{FungibleToken.Vault})
624+
case COAHandlerTxType.Call:
625+
if params.callToEVMAddress == nil || params.data == nil || params.gasLimit == nil || params.value == nil {
626+
panic("Call to EVM address, data, gas limit, and value are required for EVM call for scheduled transaction with ID \(id)")
627+
}
628+
let result = coa.call(to: params.callToEVMAddress!, data: params.data!, gasLimit: params.gasLimit!, value: params.value!)
629+
}
630+
} else {
631+
panic("Invalid scheduled transactiondata type for COA handler execution for tx with ID \(id)! Expected FlowTransactionSchedulerUtils.COAHandlerParams but got \(data.getType().identifier)")
632+
}
633+
}
634+
635+
/// Get the views supported by this handler
636+
/// @return: Array of view types
637+
access(all) view fun getViews(): [Type] {
638+
return [
639+
Type<COAHandlerView>(),
640+
Type<StoragePath>(),
641+
Type<PublicPath>(),
642+
Type<MetadataViews.Display>()
643+
]
644+
}
645+
646+
/// Resolve a view for this handler
647+
/// @param viewType: The type of view to resolve
648+
/// @return: The resolved view data, or nil if not supported
649+
access(all) fun resolveView(_ viewType: Type): AnyStruct? {
650+
if viewType == Type<COAHandlerView>() {
651+
return COAHandlerView(
652+
coaOwner: self.coaCapability.borrow()?.owner?.address,
653+
coaEVMAddress: self.coaCapability.borrow()?.address(),
654+
)
655+
}
656+
if viewType == Type<StoragePath>() {
657+
return FlowTransactionSchedulerUtils.coaHandlerStoragePath()
658+
} else if viewType == Type<PublicPath>() {
659+
return FlowTransactionSchedulerUtils.coaHandlerPublicPath()
660+
} else if viewType == Type<MetadataViews.Display>() {
661+
return MetadataViews.Display(
662+
name: "COA Scheduled Transaction Handler",
663+
description: "Scheduled Transaction Handler that can execute transactions on behalf of a COA",
664+
thumbnail: MetadataViews.HTTPFile(
665+
url: ""
666+
)
667+
)
668+
}
669+
return nil
670+
}
671+
}
672+
673+
/// Enum for COA handler execution type
674+
access(all) enum COAHandlerTxType: UInt8 {
675+
access(all) case DepositFLOW
676+
access(all) case WithdrawFLOW
677+
access(all) case Call
678+
679+
// TODO: Should we have other transaction types??
680+
}
681+
682+
access(all) struct COAHandlerParams {
683+
684+
access(all) let txType: COAHandlerTxType
685+
686+
access(all) let amount: UFix64?
687+
access(all) let callToEVMAddress: EVM.EVMAddress?
688+
access(all) let data: [UInt8]?
689+
access(all) let gasLimit: UInt64?
690+
access(all) let value: EVM.Balance?
691+
692+
init(txType: UInt8, amount: UFix64?, callToEVMAddress: [UInt8; 20]?, data: [UInt8]?, gasLimit: UInt64?, value: UFix64?) {
693+
self.txType = COAHandlerTxType(rawValue: txType)
694+
?? panic("Invalid COA transaction type enum")
695+
self.amount = amount
696+
self.callToEVMAddress = callToEVMAddress != nil ? EVM.EVMAddress(bytes: callToEVMAddress!) : nil
697+
self.data = data
698+
self.gasLimit = gasLimit
699+
if let unwrappedValue = value {
700+
self.value = EVM.Balance(attoflow: 0)
701+
self.value!.setFLOW(flow: unwrappedValue)
702+
} else {
703+
self.value = nil
704+
}
705+
}
706+
}
707+
708+
/// View struct for COA handler metadata
709+
access(all) struct COAHandlerView {
710+
access(all) let coaOwner: Address?
711+
access(all) let coaEVMAddress: EVM.EVMAddress?
712+
713+
// TODO: Should we include other metadata about the COA, like balance, code, etc???
714+
715+
init(coaOwner: Address?, coaEVMAddress: EVM.EVMAddress?) {
716+
self.coaOwner = coaOwner
717+
self.coaEVMAddress = coaEVMAddress
718+
}
719+
}
720+
721+
/// Create a COA transaction handler
722+
/// @param coaCapability: Capability to the COA resource
723+
/// @param metadata: Optional metadata about the handler
724+
/// @return: A new COATransactionHandler resource
725+
access(all) fun createCOATransactionHandler(
726+
coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
727+
flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
728+
): @COATransactionHandler {
729+
return <-create COATransactionHandler(
730+
coaCapability: coaCapability,
731+
flowTokenVaultCapability: flowTokenVaultCapability,
732+
)
733+
}
734+
564735
/********************************************
565736
566737
Scheduled Transactions Metadata Views

flow.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"source": "./contracts/FlowTransactionScheduler.cdc",
1313
"aliases": {
1414
"emulator": "f8d6e0586b0a20c7",
15+
"testnet": "8c5303eaa26202d6",
1516
"testing": "0000000000000007"
1617
}
1718
},
@@ -223,7 +224,8 @@
223224
"emulator": "127.0.0.1:3569",
224225
"mainnet": "access.mainnet.nodes.onflow.org:9000",
225226
"testing": "127.0.0.1:3569",
226-
"testnet": "access.devnet.nodes.onflow.org:9000"
227+
"testnet": "access.devnet.nodes.onflow.org:9000",
228+
"migration": "access-001.migrationtestnet1.nodes.onflow.org:9000"
227229
},
228230
"accounts": {
229231
"emulator-account": {

tests/scheduled_transaction_test_helpers.cdc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,44 @@ access(all) fun scheduleTransactionByHandler(
119119
}
120120
}
121121

122+
access(all) fun scheduleCOATransaction(
123+
timestamp: UFix64,
124+
fee: UFix64,
125+
effort: UInt64,
126+
priority: UInt8,
127+
coaTXTypeEnum: UInt8,
128+
amount: UFix64?,
129+
callToEVMAddress: [UInt8; 20]?,
130+
data: [UInt8]?,
131+
gasLimit: UInt64?,
132+
value: UFix64?,
133+
testName: String,
134+
failWithErr: String?
135+
) {
136+
var tx = Test.Transaction(
137+
code: Test.readFile("../transactions/transactionScheduler/schedule_coa_transaction.cdc"),
138+
authorizers: [admin.address],
139+
signers: [admin],
140+
arguments: [timestamp, fee, effort, priority, coaTXTypeEnum, amount, callToEVMAddress, data, gasLimit, value],
141+
)
142+
var result = Test.executeTransaction(tx)
143+
144+
if let error = failWithErr {
145+
// log(error)
146+
// log(result.error!.message)
147+
Test.expect(result, Test.beFailed())
148+
Test.assertError(
149+
result,
150+
errorMessage: error
151+
)
152+
153+
} else {
154+
if result.error != nil {
155+
Test.assert(result.error == nil, message: "Transaction failed with error: \(result.error!.message) for test case: \(testName)")
156+
}
157+
}
158+
}
159+
122160
access(all) fun cancelTransaction(id: UInt64, failWithErr: String?) {
123161
var tx = Test.Transaction(
124162
code: Test.readFile("../transactions/transactionScheduler/cancel_transaction.cdc"),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Test
2+
import BlockchainHelpers
3+
import "FlowTransactionScheduler"
4+
import "FlowToken"
5+
import "FlowTransactionSchedulerUtils"
6+
7+
import "scheduled_transaction_test_helpers.cdc"
8+
9+
access(all) var startingHeight: UInt64 = 0
10+
11+
access(all) let depositFLOWEnum: UInt8 = 0
12+
access(all) let withdrawFLOWEnum: UInt8 = 1
13+
access(all) let callEnum: UInt8 = 2
14+
15+
access(all)
16+
fun setup() {
17+
18+
var err = Test.deployContract(
19+
name: "FlowTransactionScheduler",
20+
path: "../contracts/FlowTransactionScheduler.cdc",
21+
arguments: []
22+
)
23+
Test.expect(err, Test.beNil())
24+
25+
err = Test.deployContract(
26+
name: "FlowTransactionSchedulerUtils",
27+
path: "../contracts/FlowTransactionSchedulerUtils.cdc",
28+
arguments: []
29+
)
30+
Test.expect(err, Test.beNil())
31+
32+
fundAccountWithFlow(to: admin.address, amount: 10000.0)
33+
34+
startingHeight = getCurrentBlockHeight()
35+
36+
}
37+
38+
/** ---------------------------------------------------------------------------------
39+
Transaction handler integration tests
40+
--------------------------------------------------------------------------------- */
41+
42+
access(all) fun testCOAScheduledTransactions() {
43+
44+
let currentTime = getTimestamp()
45+
let timeInFuture = currentTime + futureDelta
46+
47+
// Schedule high priority transaction
48+
scheduleCOATransaction(
49+
timestamp: timeInFuture,
50+
fee: feeAmount,
51+
effort: basicEffort,
52+
priority: highPriority,
53+
coaTXTypeEnum: depositFLOWEnum,
54+
amount: 100.0,
55+
callToEVMAddress: nil,
56+
data: nil,
57+
gasLimit: nil,
58+
value: nil,
59+
testName: "Test COA Transaction Scheduling: Deposit FLOW",
60+
failWithErr: nil
61+
)
62+
}

tests/transactionScheduler_test.cdc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ access(all) fun testEstimate() {
138138
executionEffortCost: 24.99249924
139139
)
140140

141+
let largeArray: [Int] = []
142+
while largeArray.length < 10000 {
143+
largeArray.append(1)
144+
}
145+
141146
let currentTime = getCurrentBlock().timestamp
142147
let futureTime = currentTime + 100.0
143148
let pastTime = currentTime - 100.0
@@ -215,6 +220,16 @@ access(all) fun testEstimate() {
215220
expectedTimestamp: nil,
216221
expectedError: "Invalid execution effort: \(lowPriorityMaxEffort + 1) is greater than the priority's max effort of \(lowPriorityMaxEffort)"
217222
),
223+
EstimateTestCase(
224+
name: "Excessive data size returns error",
225+
timestamp: futureTime + 11.0,
226+
priority: FlowTransactionScheduler.Priority.High,
227+
executionEffort: 1000,
228+
data: largeArray,
229+
expectedFee: nil,
230+
expectedTimestamp: nil,
231+
expectedError: "Invalid data size: 0.05337100 is greater than the maximum data size of 0.00100000MB"
232+
),
218233

219234
// Valid cases - should return EstimatedScheduledTransaction with no error
220235
EstimateTestCase(

0 commit comments

Comments
 (0)