mirror of
https://github.com/hanzo-fi/stack.git
synced 2026-10-30 20:34:43 +00:00
307 lines
8.9 KiB
Go
307 lines
8.9 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
"sync"
|
|
|
|
storageerrors "github.com/formancehq/ledger/internal/storage/sqlutils"
|
|
|
|
ledger "github.com/formancehq/ledger/internal"
|
|
"github.com/formancehq/ledger/internal/bus"
|
|
"github.com/formancehq/ledger/internal/engine/utils/batching"
|
|
"github.com/formancehq/ledger/internal/machine/vm"
|
|
"github.com/formancehq/stack/libs/go-libs/collectionutils"
|
|
"github.com/formancehq/stack/libs/go-libs/metadata"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type Parameters struct {
|
|
DryRun bool
|
|
IdempotencyKey string
|
|
}
|
|
|
|
type Commander struct {
|
|
*batching.Batcher[*ledger.ChainedLog]
|
|
store Store
|
|
locker Locker
|
|
compiler *Compiler
|
|
running sync.WaitGroup
|
|
lastTXID *big.Int
|
|
referencer *Referencer
|
|
mu sync.Mutex
|
|
|
|
lastLog *ledger.ChainedLog
|
|
monitor bus.Monitor
|
|
}
|
|
|
|
func New(store Store, locker Locker, compiler *Compiler, referencer *Referencer, monitor bus.Monitor) *Commander {
|
|
return &Commander{
|
|
store: store,
|
|
locker: locker,
|
|
compiler: compiler,
|
|
lastTXID: big.NewInt(-1),
|
|
referencer: referencer,
|
|
Batcher: batching.NewBatcher(store.InsertLogs, 1, 4096),
|
|
monitor: monitor,
|
|
}
|
|
}
|
|
|
|
func (commander *Commander) Init(ctx context.Context) error {
|
|
lastTx, err := commander.store.GetLastTransaction(ctx)
|
|
if err != nil && !storageerrors.IsNotFoundError(err) {
|
|
return err
|
|
}
|
|
if lastTx != nil {
|
|
commander.lastTXID = lastTx.ID
|
|
}
|
|
|
|
commander.lastLog, err = commander.store.GetLastLog(ctx)
|
|
if err != nil && !storageerrors.IsNotFoundError(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (commander *Commander) GetLedgerStore() Store {
|
|
return commander.store
|
|
}
|
|
|
|
func (commander *Commander) exec(ctx context.Context, parameters Parameters, script ledger.RunScript,
|
|
logComputer func(tx *ledger.Transaction, accountMetadata map[string]metadata.Metadata) *ledger.Log) (*ledger.ChainedLog, error) {
|
|
|
|
if script.Script.Plain == "" {
|
|
return nil, NewErrNoScript()
|
|
}
|
|
|
|
if script.Timestamp.IsZero() {
|
|
script.Timestamp = ledger.Now()
|
|
}
|
|
|
|
execContext := newExecutionContext(commander, parameters)
|
|
return execContext.run(ctx, func(executionContext *executionContext) (*ledger.ChainedLog, chan struct{}, error) {
|
|
if script.Reference != "" {
|
|
if err := commander.referencer.take(referenceTxReference, script.Reference); err != nil {
|
|
return nil, nil, NewErrConflict()
|
|
}
|
|
defer commander.referencer.release(referenceTxReference, script.Reference)
|
|
|
|
_, err := commander.store.GetTransactionByReference(ctx, script.Reference)
|
|
if err == nil {
|
|
return nil, nil, NewErrConflict()
|
|
}
|
|
if err != nil && !storageerrors.IsNotFoundError(err) {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
program, err := commander.compiler.Compile(script.Plain)
|
|
if err != nil {
|
|
return nil, nil, NewErrCompilationFailed(err)
|
|
}
|
|
|
|
m := vm.NewMachine(*program)
|
|
|
|
if err := m.SetVarsFromJSON(script.Vars); err != nil {
|
|
return nil, nil, NewErrCompilationFailed(err)
|
|
}
|
|
|
|
involvedAccounts, involvedSources, err := m.ResolveResources(ctx, commander.store)
|
|
if err != nil {
|
|
return nil, nil, NewErrCompilationFailed(err)
|
|
}
|
|
|
|
worldFilter := collectionutils.FilterNot(collectionutils.FilterEq("world"))
|
|
lockAccounts := Accounts{
|
|
Read: collectionutils.Filter(involvedAccounts, worldFilter),
|
|
Write: collectionutils.Filter(involvedSources, worldFilter),
|
|
}
|
|
|
|
unlock, err := commander.locker.Lock(ctx, lockAccounts)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "locking accounts for tx processing")
|
|
}
|
|
unlock(ctx)
|
|
|
|
err = m.ResolveBalances(ctx, commander.store)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "could not resolve balances")
|
|
}
|
|
|
|
result, err := vm.Run(m, script)
|
|
if err != nil {
|
|
return nil, nil, NewErrMachine(err)
|
|
}
|
|
|
|
if len(result.Postings) == 0 {
|
|
return nil, nil, NewErrNoPostings()
|
|
}
|
|
|
|
tx := ledger.NewTransaction().
|
|
WithPostings(result.Postings...).
|
|
WithMetadata(result.Metadata).
|
|
WithDate(script.Timestamp).
|
|
WithID(commander.nextTXID()).
|
|
WithReference(script.Reference)
|
|
|
|
log := logComputer(tx, result.AccountMetadata)
|
|
if parameters.IdempotencyKey != "" {
|
|
log = log.WithIdempotencyKey(parameters.IdempotencyKey)
|
|
}
|
|
|
|
return executionContext.AppendLog(ctx, log)
|
|
})
|
|
}
|
|
|
|
func (commander *Commander) CreateTransaction(ctx context.Context, parameters Parameters, script ledger.RunScript) (*ledger.Transaction, error) {
|
|
log, err := commander.exec(ctx, parameters, script, ledger.NewTransactionLog)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commander.monitor.CommittedTransactions(ctx, *log.Data.(ledger.NewTransactionLogPayload).Transaction, log.Data.(ledger.NewTransactionLogPayload).AccountMetadata)
|
|
|
|
return log.Data.(ledger.NewTransactionLogPayload).Transaction, nil
|
|
}
|
|
|
|
func (commander *Commander) SaveMeta(ctx context.Context, parameters Parameters, targetType string, targetID interface{}, m metadata.Metadata) error {
|
|
execContext := newExecutionContext(commander, parameters)
|
|
_, err := execContext.run(ctx, func(executionContext *executionContext) (*ledger.ChainedLog, chan struct{}, error) {
|
|
var (
|
|
log *ledger.Log
|
|
at = ledger.Now()
|
|
)
|
|
switch targetType {
|
|
case ledger.MetaTargetTypeTransaction:
|
|
_, err := commander.store.GetTransaction(ctx, targetID.(*big.Int))
|
|
if err != nil {
|
|
if storageerrors.IsNotFoundError(err) {
|
|
return nil, nil, newErrSaveMetadataTransactionNotFound()
|
|
}
|
|
}
|
|
log = ledger.NewSetMetadataLog(at, ledger.SetMetadataLogPayload{
|
|
TargetType: ledger.MetaTargetTypeTransaction,
|
|
TargetID: targetID.(*big.Int),
|
|
Metadata: m,
|
|
})
|
|
case ledger.MetaTargetTypeAccount:
|
|
log = ledger.NewSetMetadataLog(at, ledger.SetMetadataLogPayload{
|
|
TargetType: ledger.MetaTargetTypeAccount,
|
|
TargetID: targetID.(string),
|
|
Metadata: m,
|
|
})
|
|
default:
|
|
panic(errors.Errorf("unknown target type '%s'", targetType))
|
|
}
|
|
|
|
return executionContext.AppendLog(ctx, log)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
commander.monitor.SavedMetadata(ctx, targetType, fmt.Sprint(targetID), m)
|
|
return nil
|
|
}
|
|
|
|
func (commander *Commander) RevertTransaction(ctx context.Context, parameters Parameters, id *big.Int, force bool) (*ledger.Transaction, error) {
|
|
|
|
if err := commander.referencer.take(referenceReverts, id); err != nil {
|
|
return nil, NewErrRevertTransactionOccurring()
|
|
}
|
|
defer commander.referencer.release(referenceReverts, id)
|
|
|
|
transactionToRevert, err := commander.store.GetTransaction(ctx, id)
|
|
if err != nil {
|
|
if storageerrors.IsNotFoundError(err) {
|
|
return nil, NewErrRevertTransactionNotFound()
|
|
}
|
|
return nil, err
|
|
}
|
|
if transactionToRevert.Reverted {
|
|
return nil, NewErrRevertTransactionAlreadyReverted()
|
|
}
|
|
|
|
rt := transactionToRevert.Reverse()
|
|
rt.Metadata = ledger.MarkReverts(metadata.Metadata{}, transactionToRevert.ID)
|
|
|
|
log, err := commander.exec(ctx, parameters,
|
|
ledger.TxToScriptData(ledger.TransactionData{
|
|
Postings: rt.Postings,
|
|
Metadata: rt.Metadata,
|
|
}, force),
|
|
func(tx *ledger.Transaction, accountMetadata map[string]metadata.Metadata) *ledger.Log {
|
|
return ledger.NewRevertedTransactionLog(tx.Timestamp, transactionToRevert.ID, tx)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commander.monitor.RevertedTransaction(ctx, log.Data.(ledger.RevertedTransactionLogPayload).RevertTransaction, transactionToRevert)
|
|
|
|
return log.Data.(ledger.RevertedTransactionLogPayload).RevertTransaction, nil
|
|
}
|
|
|
|
func (commander *Commander) Close() {
|
|
commander.Batcher.Close()
|
|
commander.running.Wait()
|
|
}
|
|
|
|
func (commander *Commander) chainLog(log *ledger.Log) *ledger.ChainedLog {
|
|
commander.mu.Lock()
|
|
defer commander.mu.Unlock()
|
|
|
|
commander.lastLog = log.ChainLog(commander.lastLog)
|
|
return commander.lastLog
|
|
}
|
|
|
|
func (commander *Commander) nextTXID() *big.Int {
|
|
commander.mu.Lock()
|
|
defer commander.mu.Unlock()
|
|
|
|
ret := big.NewInt(0).Add(commander.lastTXID, big.NewInt(1))
|
|
commander.lastTXID = ret
|
|
|
|
return ret
|
|
}
|
|
|
|
func (commander *Commander) DeleteMetadata(ctx context.Context, parameters Parameters, targetType string, targetID any, key string) error {
|
|
execContext := newExecutionContext(commander, parameters)
|
|
_, err := execContext.run(ctx, func(executionContext *executionContext) (*ledger.ChainedLog, chan struct{}, error) {
|
|
var (
|
|
log *ledger.Log
|
|
at = ledger.Now()
|
|
)
|
|
switch targetType {
|
|
case ledger.MetaTargetTypeTransaction:
|
|
_, err := commander.store.GetTransaction(ctx, targetID.(*big.Int))
|
|
if err != nil {
|
|
return nil, nil, newErrDeleteMetadataTransactionNotFound()
|
|
}
|
|
log = ledger.NewDeleteMetadataLog(at, ledger.DeleteMetadataLogPayload{
|
|
TargetType: ledger.MetaTargetTypeTransaction,
|
|
TargetID: targetID.(*big.Int),
|
|
Key: key,
|
|
})
|
|
case ledger.MetaTargetTypeAccount:
|
|
log = ledger.NewDeleteMetadataLog(at, ledger.DeleteMetadataLogPayload{
|
|
TargetType: ledger.MetaTargetTypeAccount,
|
|
TargetID: targetID.(string),
|
|
Key: key,
|
|
})
|
|
default:
|
|
panic(errors.Errorf("unknown target type '%s'", targetType))
|
|
}
|
|
|
|
return executionContext.AppendLog(ctx, log)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
commander.monitor.DeletedMetadata(ctx, targetType, targetID, key)
|
|
|
|
return nil
|
|
}
|