Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions contracts/FlowTransactionSchedulerUtils.cdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import "FlowTransactionScheduler"
import "FungibleToken"
import "FlowToken"
import "EVM"
import "MetadataViews"

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

/*********************************************

COA Handler Utils

**********************************************/

access(all) view fun coaHandlerStoragePath(): StoragePath {
return /storage/coaScheduledTransactionHandler
}

access(all) view fun coaHandlerPublicPath(): PublicPath {
return /public/coaScheduledTransactionHandler
}

/// COATransactionHandler is a resource that wraps a capability to a COA (Contract Owned Account)
/// and implements the TransactionHandler interface to allow scheduling transactions for COAs.
/// This handler enables users to schedule transactions that will be executed on behalf of their COA.
access(all) resource COATransactionHandler: FlowTransactionScheduler.TransactionHandler {
/// The capability to the COA resource
access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>

/// The capability to the FlowToken vault
access(self) let flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>

init(coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
)
{
self.coaCapability = coaCapability
self.flowTokenVaultCapability = flowTokenVaultCapability
}

/// Execute the scheduled transaction using the COA
/// @param id: The ID of the scheduled transaction
/// @param data: Optional data passed to the transaction execution. In this case, the data needs to be a COAHandlerParams struct with valid values.
access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
let coa = self.coaCapability.borrow()
?? panic("COA capability is invalid or expired for scheduled transaction with ID \(id)")

if let params = data as? COAHandlerParams {
switch params.txType {
case COAHandlerTxType.DepositFLOW:
if params.amount == nil {
panic("Amount is required for deposit for scheduled transaction with ID \(id)")
}
let vault = self.flowTokenVaultCapability.borrow()
?? panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id)")
coa.deposit(from: <-vault.withdraw(amount: params.amount!) as! @FlowToken.Vault)
case COAHandlerTxType.WithdrawFLOW:
if params.amount == nil {
panic("Amount is required for withdrawal from COA for scheduled transaction with ID \(id)")
}
let vault = self.flowTokenVaultCapability.borrow()
?? panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id)")
let amount = EVM.Balance(attoflow: 0)
amount.setFLOW(flow: params.amount!)
vault.deposit(from: <-coa.withdraw(balance: amount) as! @{FungibleToken.Vault})
case COAHandlerTxType.Call:
if params.callToEVMAddress == nil || params.data == nil || params.gasLimit == nil || params.value == nil {
panic("Call to EVM address, data, gas limit, and value are required for EVM call for scheduled transaction with ID \(id)")
}
let result = coa.call(to: params.callToEVMAddress!, data: params.data!, gasLimit: params.gasLimit!, value: params.value!)
}
} else {
panic("Invalid scheduled transactiondata type for COA handler execution for tx with ID \(id)! Expected FlowTransactionSchedulerUtils.COAHandlerParams but got \(data.getType().identifier)")
}
}

/// Get the views supported by this handler
/// @return: Array of view types
access(all) view fun getViews(): [Type] {
return [
Type<COAHandlerView>(),
Type<StoragePath>(),
Type<PublicPath>(),
Type<MetadataViews.Display>()
]
}

/// Resolve a view for this handler
/// @param viewType: The type of view to resolve
/// @return: The resolved view data, or nil if not supported
access(all) fun resolveView(_ viewType: Type): AnyStruct? {
if viewType == Type<COAHandlerView>() {
return COAHandlerView(
coaOwner: self.coaCapability.borrow()?.owner?.address,
coaEVMAddress: self.coaCapability.borrow()?.address(),
)
}
if viewType == Type<StoragePath>() {
return FlowTransactionSchedulerUtils.coaHandlerStoragePath()
} else if viewType == Type<PublicPath>() {
return FlowTransactionSchedulerUtils.coaHandlerPublicPath()
} else if viewType == Type<MetadataViews.Display>() {
return MetadataViews.Display(
name: "COA Scheduled Transaction Handler",
description: "Scheduled Transaction Handler that can execute transactions on behalf of a COA",
thumbnail: MetadataViews.HTTPFile(
url: ""
)
)
}
return nil
}
}

/// Enum for COA handler execution type
access(all) enum COAHandlerTxType: UInt8 {
access(all) case DepositFLOW
access(all) case WithdrawFLOW
access(all) case Call

// TODO: Should we have other transaction types??
}

access(all) struct COAHandlerParams {

access(all) let txType: COAHandlerTxType

access(all) let amount: UFix64?
access(all) let callToEVMAddress: EVM.EVMAddress?
access(all) let data: [UInt8]?
access(all) let gasLimit: UInt64?
access(all) let value: EVM.Balance?

init(txType: UInt8, amount: UFix64?, callToEVMAddress: [UInt8; 20]?, data: [UInt8]?, gasLimit: UInt64?, value: UFix64?) {
self.txType = COAHandlerTxType(rawValue: txType)
?? panic("Invalid COA transaction type enum")
self.amount = amount
self.callToEVMAddress = callToEVMAddress != nil ? EVM.EVMAddress(bytes: callToEVMAddress!) : nil
self.data = data
self.gasLimit = gasLimit
if let unwrappedValue = value {
self.value = EVM.Balance(attoflow: 0)
self.value!.setFLOW(flow: unwrappedValue)
} else {
self.value = nil
}
}
}

/// View struct for COA handler metadata
access(all) struct COAHandlerView {
access(all) let coaOwner: Address?
access(all) let coaEVMAddress: EVM.EVMAddress?

// TODO: Should we include other metadata about the COA, like balance, code, etc???

init(coaOwner: Address?, coaEVMAddress: EVM.EVMAddress?) {
self.coaOwner = coaOwner
self.coaEVMAddress = coaEVMAddress
}
}

/// Create a COA transaction handler
/// @param coaCapability: Capability to the COA resource
/// @param metadata: Optional metadata about the handler
/// @return: A new COATransactionHandler resource
access(all) fun createCOATransactionHandler(
coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
): @COATransactionHandler {
return <-create COATransactionHandler(
coaCapability: coaCapability,
flowTokenVaultCapability: flowTokenVaultCapability,
)
}

/********************************************

Scheduled Transactions Metadata Views
Expand Down
4 changes: 3 additions & 1 deletion flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"source": "./contracts/FlowTransactionScheduler.cdc",
"aliases": {
"emulator": "f8d6e0586b0a20c7",
"testnet": "8c5303eaa26202d6",
"testing": "0000000000000007"
}
},
Expand Down Expand Up @@ -223,7 +224,8 @@
"emulator": "127.0.0.1:3569",
"mainnet": "access.mainnet.nodes.onflow.org:9000",
"testing": "127.0.0.1:3569",
"testnet": "access.devnet.nodes.onflow.org:9000"
"testnet": "access.devnet.nodes.onflow.org:9000",
"migration": "access-001.migrationtestnet1.nodes.onflow.org:9000"
},
"accounts": {
"emulator-account": {
Expand Down
38 changes: 38 additions & 0 deletions tests/scheduled_transaction_test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,44 @@ access(all) fun scheduleTransactionByHandler(
}
}

access(all) fun scheduleCOATransaction(
timestamp: UFix64,
fee: UFix64,
effort: UInt64,
priority: UInt8,
coaTXTypeEnum: UInt8,
amount: UFix64?,
callToEVMAddress: [UInt8; 20]?,
data: [UInt8]?,
gasLimit: UInt64?,
value: UFix64?,
testName: String,
failWithErr: String?
) {
var tx = Test.Transaction(
code: Test.readFile("../transactions/transactionScheduler/schedule_coa_transaction.cdc"),
authorizers: [admin.address],
signers: [admin],
arguments: [timestamp, fee, effort, priority, coaTXTypeEnum, amount, callToEVMAddress, data, gasLimit, value],
)
var result = Test.executeTransaction(tx)

if let error = failWithErr {
// log(error)
// log(result.error!.message)
Test.expect(result, Test.beFailed())
Test.assertError(
result,
errorMessage: error
)

} else {
if result.error != nil {
Test.assert(result.error == nil, message: "Transaction failed with error: \(result.error!.message) for test case: \(testName)")
}
}
}

access(all) fun cancelTransaction(id: UInt64, failWithErr: String?) {
var tx = Test.Transaction(
code: Test.readFile("../transactions/transactionScheduler/cancel_transaction.cdc"),
Expand Down
62 changes: 62 additions & 0 deletions tests/transactionScheduler_coa_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Test
import BlockchainHelpers
import "FlowTransactionScheduler"
import "FlowToken"
import "FlowTransactionSchedulerUtils"

import "scheduled_transaction_test_helpers.cdc"

access(all) var startingHeight: UInt64 = 0

access(all) let depositFLOWEnum: UInt8 = 0
access(all) let withdrawFLOWEnum: UInt8 = 1
access(all) let callEnum: UInt8 = 2

access(all)
fun setup() {

var err = Test.deployContract(
name: "FlowTransactionScheduler",
path: "../contracts/FlowTransactionScheduler.cdc",
arguments: []
)
Test.expect(err, Test.beNil())

err = Test.deployContract(
name: "FlowTransactionSchedulerUtils",
path: "../contracts/FlowTransactionSchedulerUtils.cdc",
arguments: []
)
Test.expect(err, Test.beNil())

fundAccountWithFlow(to: admin.address, amount: 10000.0)

startingHeight = getCurrentBlockHeight()

}

/** ---------------------------------------------------------------------------------
Transaction handler integration tests
--------------------------------------------------------------------------------- */

access(all) fun testCOAScheduledTransactions() {

let currentTime = getTimestamp()
let timeInFuture = currentTime + futureDelta

// Schedule high priority transaction
scheduleCOATransaction(
timestamp: timeInFuture,
fee: feeAmount,
effort: basicEffort,
priority: highPriority,
coaTXTypeEnum: depositFLOWEnum,
amount: 100.0,
callToEVMAddress: nil,
data: nil,
gasLimit: nil,
value: nil,
testName: "Test COA Transaction Scheduling: Deposit FLOW",
failWithErr: nil
)
}
15 changes: 15 additions & 0 deletions tests/transactionScheduler_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ access(all) fun testEstimate() {
executionEffortCost: 24.99249924
)

let largeArray: [Int] = []
while largeArray.length < 10000 {
largeArray.append(1)
}

let currentTime = getCurrentBlock().timestamp
let futureTime = currentTime + 100.0
let pastTime = currentTime - 100.0
Expand Down Expand Up @@ -215,6 +220,16 @@ access(all) fun testEstimate() {
expectedTimestamp: nil,
expectedError: "Invalid execution effort: \(lowPriorityMaxEffort + 1) is greater than the priority's max effort of \(lowPriorityMaxEffort)"
),
EstimateTestCase(
name: "Excessive data size returns error",
timestamp: futureTime + 11.0,
priority: FlowTransactionScheduler.Priority.High,
executionEffort: 1000,
data: largeArray,
expectedFee: nil,
expectedTimestamp: nil,
expectedError: "Invalid data size: 0.05337100 is greater than the maximum data size of 0.00100000MB"
),

// Valid cases - should return EstimatedScheduledTransaction with no error
EstimateTestCase(
Expand Down
Loading
Loading