diff --git a/chain_sync.go b/chain_sync.go index 6978a80..74395a2 100644 --- a/chain_sync.go +++ b/chain_sync.go @@ -71,6 +71,7 @@ type ChainSyncOptions struct { points chainsync.Points // points to attempt initial intersection reconnect bool // reconnect to ogmios if connection drops store Store // store of points + useV6 bool // useV6 decides using v6 or v5 payload } func buildChainSyncOptions(opts ...ChainSyncOption) ChainSyncOptions { @@ -101,6 +102,13 @@ func WithPoints(points ...chainsync.Point) ChainSyncOption { } } +// WithUseV6 decides using ogmios v6 or v5 interface +func WithUseV6(useV6 bool) ChainSyncOption { + return func(opts *ChainSyncOptions) { + opts.useV6 = useV6 + } +} + // WithReconnect attempt to reconnect to ogmios if connection drops func WithReconnect(enabled bool) ChainSyncOption { return func(opts *ChainSyncOptions) { @@ -173,8 +181,14 @@ func (c *Client) doChainSync(ctx context.Context, callback ChainSyncFunc, option c.logger.Error(err, "failed to connect to ogmios", KV("endpoint", c.options.endpoint)) return fmt.Errorf("failed to connect to ogmios, %v: %w", c.options.endpoint, err) } - - init, err := getInit(ctx, options.store, options.points...) + var init, next []byte + if options.useV6 { + next = []byte(`{"jsonrpc":"2.0","method":"nextBlock"}`) + init, err = getInitV6(ctx, options.store, options.points...) + } else { + next = []byte(`{"type":"jsonwsp/request","version":"1.0","servicename":"ogmios","methodname":"RequestNext","args":{}}`) + init, err = getInit(ctx, options.store, options.points...) + } if err != nil { c.logger.Error(err, "failed to create init message") return fmt.Errorf("failed to create init message: %w", err) @@ -221,7 +235,6 @@ func (c *Client) doChainSync(ctx context.Context, callback ChainSyncFunc, option return fmt.Errorf("failed to write FindIntersect: %w", err) } - next := []byte(`{"type":"jsonwsp/request","version":"1.0","servicename":"ogmios","methodname":"RequestNext","args":{}}`) for { select { case <-ctx.Done(): @@ -355,6 +368,34 @@ func getInit(ctx context.Context, store Store, pp ...chainsync.Point) (data []by return json.Marshal(init) } +func getInitV6(ctx context.Context, store Store, pp ...chainsync.Point) (data []byte, err error) { + points, err := store.Load(ctx) + if err != nil { + return nil, fmt.Errorf("failed to retrieve points from store: %w", err) + } + if len(points) == 0 { + points = append(points, pp...) + } + if len(points) == 0 { + points = append(points, chainsync.Origin) + } + sort.Sort(points) + if len(points) > 5 { + points = points[0:5] + } + pointsV6 := points.ConvertToV6() + init := Map{ + "jsonrpc": "2.0", + "method": "findIntersection", + "params": Map{ + "points": pointsV6, + }, + "id": "findIntersect", + } + + return json.Marshal(init) +} + // getPoint returns the first point from the list of json encoded chainsync.Responses provided // multiple Responses allow for the possibility of a Rollback being included in the set func getPoint(data ...[]byte) (chainsync.Point, bool) { diff --git a/cmd/ogmigo/main.go b/cmd/ogmigo/main.go index abdc67b..84927a1 100644 --- a/cmd/ogmigo/main.go +++ b/cmd/ogmigo/main.go @@ -16,8 +16,10 @@ package main import ( "context" + "encoding/json" "fmt" "github.com/thuannguyen2010/ogmigo" + "github.com/thuannguyen2010/ogmigo/ouroboros/chainsync" "github.com/urfave/cli/v2" "log" "os" @@ -65,16 +67,64 @@ func main() { func action(_ *cli.Context) error { client := ogmigo.New( - ogmigo.WithEndpoint("ws://localhost:1337"), + ogmigo.WithEndpoint("ws://172.0.0.1:1337"), ogmigo.WithLogger(ogmigo.DefaultLogger), ) - ctx := context.Background() - redeemer, err := client.EvaluateTx(ctx, "") + var ( + ctx = context.Background() + points chainsync.Points + ) + points = []chainsync.Point{ + chainsync.PointStruct{ + BlockNo: 1009189, + Hash: "b95423b8778536cf9a53e8855cef2bd8702f79ffa3c918b8857437cfb238474d", + Slot: 23003752, + }.Point(), + } + //useV6 := false + useV6 := true + var callback ogmigo.ChainSyncFunc = func(ctx context.Context, data []byte) error { + var response chainsync.Response + if useV6 { + var resV6 chainsync.ResponseV6 + if err := json.Unmarshal(data, &resV6); err != nil { + return err + } + response = resV6.ConvertToV5() + } else { + if err := json.Unmarshal(data, &response); err != nil { + return err + } + } + + if response.Result == nil { + return nil + } + if response.Result.RollForward != nil { + ps := response.Result.RollForward.Block.PointStruct() + fmt.Println("blockNo", ps.BlockNo, "hash", ps.Hash, "slot", ps.Slot) + } + if response.Result.RollBackward != nil { + return nil + } + + //ps := response.Result.RollForward.Block.PointStruct() + //fmt.Printf("slot=%v hash=%v block=%v\n", ps.Slot, ps.Hash, ps.BlockNo) + + return nil + } + closer, err := client.ChainSync(ctx, callback, + ogmigo.WithPoints(points...), + ogmigo.WithReconnect(true), + ogmigo.WithUseV6(useV6), + ) + if err != nil { - panic(err) + return err } - fmt.Println(redeemer) + defer closer.Close() + stop := make(chan os.Signal, 1) signal.Notify(stop, os.Kill, os.Interrupt) diff --git a/ouroboros/chainsync/types.go b/ouroboros/chainsync/types.go index 26e61f9..41d7180 100644 --- a/ouroboros/chainsync/types.go +++ b/ouroboros/chainsync/types.go @@ -558,3 +558,15 @@ func (a RedeemerKey) Index() int { } return 0 } + +type EvaluationResult []EvaluationItem + +type EvaluationItem struct { + Validator string `json:"validator"` + Budget Budget `json:"budget"` +} + +type Budget struct { + Memory int64 `json:"memory"` + Steps int64 `json:"cpu"` +} diff --git a/ouroboros/chainsync/types_v6.go b/ouroboros/chainsync/types_v6.go new file mode 100644 index 0000000..bbe3340 --- /dev/null +++ b/ouroboros/chainsync/types_v6.go @@ -0,0 +1,274 @@ +package chainsync + +import ( + "encoding/json" + "fmt" + "github.com/thuannguyen2010/ogmigo/ouroboros/chainsync/num" +) + +type ResponseV6 struct { + Jsonrpc string `json:"jsonrpc,omitempty"` + Method string `json:"method,omitempty"` + Result *ResultV6 `json:"result,omitempty"` +} + +type ResultV6 struct { + Direction string `json:"direction,omitempty"` + BlockV6 *BlockV6 `json:"block,omitempty"` + Point *PointV6 `json:"point,omitempty"` // roll backward (nextBlock) + Intersection *PointV6 `json:"intersection,omitempty"` // intersection found (findIntersection) + Error *Error `json:"error,omitempty"` // intersection not found (findIntersection) +} + +type Error struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type BlockV6 struct { + Type string `json:"type,omitempty"` + Era string `json:"era,omitempty"` + ID string `json:"id,omitempty"` + Height uint64 `json:"height,omitempty"` + Slot uint64 `json:"slot,omitempty"` + Transactions []Transaction `json:"transactions,omitempty"` +} + +type Transaction struct { + ID string `json:"id,omitempty"` + Spends string `json:"spends,omitempty"` + TxInputs []TxInV6 `json:"inputs,omitempty"` + TxOutputs []TxOutV6 `json:"outputs,omitempty"` + Datums map[string]HexData `json:"datums,omitempty"` + Redeemers json.RawMessage `json:"redeemers,omitempty"` + Fee Fee `json:"fee"` +} + +type Fee struct { + Lovelace num.Int `json:"lovelace,omitempty"` +} + +type TxInV6 struct { + Transaction struct { + ID string `json:"id,omitempty"` + } `json:"transaction,omitempty"` + Index int `json:"index,omitempty"` +} + +type TxOutV6 struct { + Address string `json:"address,omitempty"` + Value map[string]map[string]num.Int + Datum string `json:"datum"` + DatumHash string `json:"datumHash"` +} + +type PointV6 struct { + pointType PointType + pointString PointString + pointStruct *PointStructV6 +} + +type PointsV6 []PointV6 + +type PointStructV6 struct { + ID string `json:"id,omitempty"` + Slot uint64 `json:"slot,omitempty"` +} + +func (p *PointV6) UnmarshalJSON(data []byte) error { + switch { + case data[0] == '"': + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("failed to unmarshal Point, %v: %w", string(data), err) + } + + *p = PointV6{ + pointType: PointTypeString, + pointString: PointString(s), + } + + default: + var ps PointStructV6 + if err := json.Unmarshal(data, &ps); err != nil { + return fmt.Errorf("failed to unmarshal Point, %v: %w", string(data), err) + } + + *p = PointV6{ + pointType: PointTypeStruct, + pointStruct: &ps, + } + } + + return nil +} + +func (p *PointV6) convertToV5() Point { + var pointStruct *PointStruct + if p.pointStruct != nil { + pointStruct = &PointStruct{ + Hash: p.pointStruct.ID, + Slot: p.pointStruct.Slot, + } + } + return Point{ + pointType: p.pointType, + pointString: p.pointString, + pointStruct: pointStruct, + } +} + +func (p PointV6) MarshalJSON() ([]byte, error) { + switch p.pointType { + case PointTypeString: + return json.Marshal(p.pointString) + case PointTypeStruct: + return json.Marshal(p.pointStruct) + default: + return nil, fmt.Errorf("unable to unmarshal Point: unknown type") + } +} + +func (p *Point) ConvertToV6() PointV6 { + var pointStruct *PointStructV6 + if p.pointStruct != nil { + pointStruct = &PointStructV6{ + ID: p.pointStruct.Hash, + Slot: p.pointStruct.Slot, + } + } + return PointV6{ + pointType: p.pointType, + pointString: p.pointString, + pointStruct: pointStruct, + } +} + +func (pp Points) ConvertToV6() PointsV6 { + var result PointsV6 + for _, p := range pp { + result = append(result, p.ConvertToV6()) + } + return result +} + +func (responseV6 ResponseV6) ConvertToV5() (response Response) { + response = Response{ + Type: responseV6.Jsonrpc, + Version: responseV6.Jsonrpc, + ServiceName: "ogmios", + MethodName: responseV6.Method, + Result: nil, + } + // intersection not found + if responseV6.Result.Error != nil { + response.Result = &Result{ + IntersectionNotFound: &IntersectionNotFound{}, + } + return + } + // defend result is nil + if responseV6.Result == nil { + return + } + // intersection found + if responseV6.Result.Intersection != nil { + response.Result = &Result{ + IntersectionFound: &IntersectionFound{ + Point: responseV6.Result.Intersection.convertToV5(), + }, + } + return + } + // roll backward + if responseV6.Result.Direction == "backward" { + response.Result = &Result{ + RollBackward: &RollBackward{}, + } + } + // roll forward block + if responseV6.Result.BlockV6 == nil { + return + } + var txs []Tx + for _, txV6 := range responseV6.Result.BlockV6.Transactions { + var txIns []TxIn + for _, txInV6 := range txV6.TxInputs { + txIns = append(txIns, TxIn{ + TxHash: txInV6.Transaction.ID, + Index: txInV6.Index, + }) + } + + var txOuts TxOuts + for _, txOutV6 := range txV6.TxOutputs { + var coins num.Int + assets := make(map[AssetID]num.Int) + for policyID, assetNameMap := range txOutV6.Value { + if policyID == "ada" { + for assetName, val := range assetNameMap { + if assetName == "lovelace" { + coins = val + } + } + } else { + for assetName, val := range assetNameMap { + assetID := AssetID(fmt.Sprintf("%s.%s", policyID, assetName)) + assets[assetID] = val + } + } + } + txOuts = append(txOuts, TxOut{ + Address: txOutV6.Address, + Datum: txOutV6.Datum, + DatumHash: txOutV6.DatumHash, + Value: Value{ + Coins: coins, + Assets: assets, + }, + }) + } + txs = append(txs, Tx{ + ID: txV6.ID, + Body: TxBody{ + Fee: txV6.Fee.Lovelace, + Inputs: txIns, + Outputs: txOuts, + }, + Witness: Witness{ + Datums: txV6.Datums, + Redeemers: txV6.Redeemers, + }, + }) + } + block := Block{ + Body: txs, + Header: BlockHeader{ + BlockHeight: responseV6.Result.BlockV6.Height, + Slot: responseV6.Result.BlockV6.Slot, + }, + HeaderHash: responseV6.Result.BlockV6.ID, + } + var rollForwardBlock RollForwardBlock + switch responseV6.Result.BlockV6.Era { + case "shelley", "allegra", "mary", "alonzo", "babbage", "conway": + rollForwardBlock = RollForwardBlock{ + Babbage: &block, + } + default: + rollForwardBlock = RollForwardBlock{} + } + + result := Result{ + IntersectionFound: nil, + IntersectionNotFound: nil, + RollForward: &RollForward{ + Block: rollForwardBlock, + }, + RollBackward: nil, + } + + response.Result = &result + return response + +} diff --git a/tx_evalution.go b/tx_evalution.go index eb9a8b5..6c7a696 100644 --- a/tx_evalution.go +++ b/tx_evalution.go @@ -46,6 +46,46 @@ func (c *Client) EvaluateTx(ctx context.Context, cborHex string) (redeemer chain return readEvaluateTx(raw) } +func readEvaluateTx(data []byte) (chainsync.Redeemer, error) { + value, dataType, _, err := jsonparser.Get(data, "result", "EvaluationFailure") + if err != nil { + if errors.Is(err, jsonparser.KeyPathNotFoundError) { + redeemerRaw, _, _, err := jsonparser.Get(data, "result", "EvaluationResult") + if err != nil { + return nil, err + } + var v chainsync.Redeemer + err = json.Unmarshal(redeemerRaw, &v) + if err != nil { + return nil, fmt.Errorf("cannot parse result: %v to redeemer: %w", string(value), err) + } + return v, nil + } + return nil, fmt.Errorf("failed to parse EvaluateTx response: %w", err) + } + + switch dataType { + case jsonparser.Object: + return nil, EvaluateTxError{message: value} + default: + return nil, fmt.Errorf("EvaluateTx failed: %v", string(value)) + } +} + +// EvaluateTxV6 evaluate the execution units of scripts present in a given transaction, without actually submitting the transaction +// https://ogmios.dev/mini-protocols/local-tx-submission/#evaluatetx +func (c *Client) EvaluateTxV6(ctx context.Context, cborHex string) (redeemer chainsync.Redeemer, err error) { + var ( + payload = makePayloadV6("evaluateTransaction", Map{"transaction": Map{"cbor": cborHex}}) + raw json.RawMessage + ) + if err := c.query(ctx, payload, &raw); err != nil { + return nil, fmt.Errorf("failed to evaluate tx: %w", err) + } + + return readEvaluateTxV6(raw) +} + // EvaluateTxError encapsulates the EvaluateTx errors and allows the results to be parsed type EvaluateTxError struct { message json.RawMessage @@ -61,20 +101,27 @@ func (s EvaluateTxError) Error() string { return fmt.Sprintf("EvaluateTx failed: %v", string(s.message)) } -func readEvaluateTx(data []byte) (chainsync.Redeemer, error) { - value, dataType, _, err := jsonparser.Get(data, "result", "EvaluationFailure") +func readEvaluateTxV6(data []byte) (chainsync.Redeemer, error) { + value, dataType, _, err := jsonparser.Get(data, "error") if err != nil { if errors.Is(err, jsonparser.KeyPathNotFoundError) { - redeemerRaw, _, _, err := jsonparser.Get(data, "result", "EvaluationResult") + redeemerRaw, _, _, err := jsonparser.Get(data, "result") if err != nil { return nil, err } - var v chainsync.Redeemer + var v chainsync.EvaluationResult err = json.Unmarshal(redeemerRaw, &v) if err != nil { return nil, fmt.Errorf("cannot parse result: %v to redeemer: %w", string(value), err) } - return v, nil + result := make(chainsync.Redeemer) + for _, item := range v { + var redeemerValue chainsync.RedeemerValue + redeemerValue.Memory = item.Budget.Memory + redeemerValue.Steps = item.Budget.Steps + result[chainsync.RedeemerKey(item.Validator)] = redeemerValue + } + return result, nil } return nil, fmt.Errorf("failed to parse EvaluateTx response: %w", err) } diff --git a/util.go b/util.go index 137600e..fd042f4 100644 --- a/util.go +++ b/util.go @@ -56,3 +56,11 @@ func makePayload(methodName string, args Map) Map { "args": args, } } + +func makePayloadV6(method string, params Map) Map { + return Map{ + "jsonrpc": "2.0", + "method": method, + "params": params, + } +}