Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction Chaining Method #1508

Closed
notJoon opened this issue Jan 10, 2024 · 4 comments
Closed

Transaction Chaining Method #1508

notJoon opened this issue Jan 10, 2024 · 4 comments

Comments

@notJoon
Copy link
Member

notJoon commented Jan 10, 2024

Description

Transaction chaining is an action that executes the latter transaction based on the result of the previous transaction.

For example with GnoSwap, we have a feature called “One-click Staking”, which composed of two transactions. The first transaction is about adding a pool, then based on the result of the first transaction, the second transaction of staking will be followed. This is similar to method chaining or the pipe operation(|) in UNIX.

Adding the transaction chaining feature would allow dApps to provide user-friendly and convinient features, like the One-click Staking mentioned above. I think it's a useful feature in situations where chain actions are needed.

To implement enhanced transaction chaining, the following conditions must be satisfied:

  1. Chaining Order Guarantee: The execution order of transactions must be clearly defined and adhered to.
  2. Predictable Return Values: Each function in the chain must have an expectable return value to ensure consistency in transaction processing.
  3. State Reversion on Failure: If any transaction in the chain fails, all changes made during the chain execution must be reverted to maintain data integrity.

Below is the pseudocode for implementing chain transactions:

type TxResult struct {
	Ok   interface{}
	Err  error
}

type TxFunc func(input interface{}) TxResult
type TxChain struct {
	txs  []TxFunc
}

func NewChain() *TxChain {
	return &TxChain{}
}

// Add adds a new transaction functions to the chain
func (t *TxChain) Add(fn ...TxFunc) *TxChain {
	t.txs = append(t.txs, fn...)
	return t
}

// Execute executes all transactions sequentially
func (t *TxChain) Execute(input interface{}) (interface{}, error) {
	var result      interface{} = input
	var executedTxs []TxFunc // Keep track of successfully executed transactions

	for _, tx := range t.txs {
		if res := tx(result); res.Err != nil {
			t.revertState(executedTxs, input)
			return nil, res.Err
		} else {
			result = res.Ok
			executedTxs = append(executedTxs, tx)
		}
	}
	return result, nil
}

// revertState reverts the state changes made by the executed transactions
func (t *TxChain) revertState(executedTxs []TxFunc, initialState interface{}) {
	/* Revert all states */ 
}

As an aside, if introducing new syntax, adopting Elixir's pipe operator might not be a bad idea. But it’s cumbersome to implement, so may not feasible.

tx1 |> tx2 |> ... |> txn

A way of implementing this feature has not been decided yet (I believe there are various options for this that I’m not aware of), so I’m open to any ideas/opinions from others on this. If you have any good suggestions, please leave them in the comments below. Thank you.

Additional notes

In the Cosmos SDK, I've seen that messages are processed atomically, and I think extending this functionality would allow for pipelining.


cc: @dongwon8247 @r3v4s @mconcat

@moul
Copy link
Member

moul commented Jan 18, 2024

I recommend utilizing the current transaction batch/composition logics, which appear to be the "gno way" approach. This can be accomplished by creating batch-oriented contracts accessible to all through maketx call or by directly using maketx run.

You can find the pull request for the maketx run feature here: #1001. Additionally, there is an example where 10 calls to the tests realm are simulated in a single for loop here: https://gist.github.com/moul/ccf1e2aff64e7a1f0c5ca5e2d98d7e9a. Since it's Gno, you have the flexibility to implement any desired logic, efficiently.

I recommend delaying the discussion of creating a pseudolanguage for advanced transaction parsing within the mempool. Let's first identify the limitations of the current system. After that, we can focus on improving and expanding the mempool. It's important to keep the mempool simple, fast, and efficient.

Here are some suggestions to proceed:

  • Prioritize integrating maketx run into the SDKs (js/ts and Go).
  • Consider writing wrappers to simplify contract development, allowing you to focus on specific code segments instead of writing full contracts. This approach would be similar to how q_eval works but with multiline support.
  • Consider developing a wrapper that generates a maketx run based on your intended calls. This reminds me of the composition logic in Python's Alchemy library, which functions as an "ORM" for transactions.
  • Think about developing p/xxx libraries to support common patterns for chaining conditional transactions.

@thehowl
Copy link
Member

thehowl commented Jan 26, 2024

Does manfred's answer satisfy your question/problems? Are there action items on this issue? @notJoon

@notJoon
Copy link
Member Author

notJoon commented Jan 26, 2024

@thehowl Oh, yes I think it's best to make it a function for now. no follow up action-item yet. should I close this issue or leave it open?

@thehowl thehowl closed this as completed Jan 26, 2024
@thehowl
Copy link
Member

thehowl commented Jan 26, 2024

Let's close it :) can always open a new one should you have a usecase where MsgRun does not satisfy your needs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

No branches or pull requests

3 participants