feat: stickerpacks service
This commit is contained in:
parent
37b06cd3b1
commit
0a758b0f7d
|
@ -0,0 +1 @@
|
|||
UPDATE TABLE settings SET stickers_recent_stickers = NULL WHERE synthetic_id = 'id';
|
|
@ -0,0 +1,122 @@
|
|||
package contracts
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/contracts/registrar"
|
||||
"github.com/status-im/status-go/contracts/resolver"
|
||||
"github.com/status-im/status-go/contracts/snt"
|
||||
"github.com/status-im/status-go/contracts/stickers"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
)
|
||||
|
||||
type ContractMaker struct {
|
||||
RPCClient *rpc.Client
|
||||
}
|
||||
|
||||
func (c *ContractMaker) NewRegistry(chainID uint64) (*resolver.ENSRegistryWithFallback, error) {
|
||||
contractAddr, err := resolver.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resolver.NewENSRegistryWithFallback(
|
||||
contractAddr,
|
||||
backend,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ContractMaker) NewPublicResolver(chainID uint64, resolverAddress *common.Address) (*resolver.PublicResolver, error) {
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resolver.NewPublicResolver(*resolverAddress, backend)
|
||||
}
|
||||
|
||||
func (c *ContractMaker) NewUsernameRegistrar(chainID uint64) (*registrar.UsernameRegistrar, error) {
|
||||
contractAddr, err := registrar.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return registrar.NewUsernameRegistrar(
|
||||
contractAddr,
|
||||
backend,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ContractMaker) NewSNT(chainID uint64) (*snt.SNT, error) {
|
||||
contractAddr, err := snt.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return snt.NewSNT(contractAddr, backend)
|
||||
}
|
||||
|
||||
func (c *ContractMaker) NewStickerType(chainID uint64) (*stickers.StickerType, error) {
|
||||
contractAddr, err := stickers.StickerTypeContractAddress(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stickers.NewStickerType(
|
||||
contractAddr,
|
||||
backend,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ContractMaker) NewStickerMarket(chainID uint64) (*stickers.StickerMarket, error) {
|
||||
contractAddr, err := stickers.StickerMarketContractAddress(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stickers.NewStickerMarket(
|
||||
contractAddr,
|
||||
backend,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ContractMaker) NewStickerPack(chainID uint64) (*stickers.StickerPack, error) {
|
||||
contractAddr, err := stickers.StickerPackContractAddress(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stickers.NewStickerPack(
|
||||
contractAddr,
|
||||
backend,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package registrar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var errorNotAvailableOnChainID = errors.New("not available for chainID")
|
||||
|
||||
var contractAddressByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0xDB5ac1a559b02E12F29fC0eC0e37Be8E046DEF49"), // mainnet
|
||||
3: common.HexToAddress("0xdaae165beb8c06e0b7613168138ebba774aff071"), // ropsten
|
||||
}
|
||||
|
||||
func ContractAddress(chainID uint64) (common.Address, error) {
|
||||
addr, exists := contractAddressByChainID[chainID]
|
||||
if !exists {
|
||||
return *new(common.Address), errorNotAvailableOnChainID
|
||||
}
|
||||
return addr, nil
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var errorNotAvailableOnChainID = errors.New("not available for chainID")
|
||||
|
||||
var contractAddressByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // mainnet
|
||||
3: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // ropsten
|
||||
}
|
||||
|
||||
func ContractAddress(chainID uint64) (common.Address, error) {
|
||||
addr, exists := contractAddressByChainID[chainID]
|
||||
if !exists {
|
||||
return *new(common.Address), errorNotAvailableOnChainID
|
||||
}
|
||||
return addr, nil
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package snt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var errorNotAvailableOnChainID = errors.New("not available for chainID")
|
||||
|
||||
var contractAddressByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e"), // mainnet
|
||||
3: common.HexToAddress("0xc55cf4b03948d7ebc8b9e8bad92643703811d162"), // ropsten
|
||||
}
|
||||
|
||||
func ContractAddress(chainID uint64) (common.Address, error) {
|
||||
addr, exists := contractAddressByChainID[chainID]
|
||||
if !exists {
|
||||
return *new(common.Address), errorNotAvailableOnChainID
|
||||
}
|
||||
return addr, nil
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package snt
|
||||
|
||||
//go:generate abigen -sol snt.sol -pkg snt -out snt.go
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated - DO NOT EDIT.
|
||||
// This file is a generated binding and any manual changes will be lost.
|
||||
|
||||
package erc20
|
||||
package snt
|
||||
|
||||
import (
|
||||
"math/big"
|
|
@ -0,0 +1,48 @@
|
|||
package stickers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var errorNotAvailableOnChainID = errors.New("not available for chainID")
|
||||
|
||||
var stickerTypeByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0x0577215622f43a39f4bc9640806dfea9b10d2a36"), // mainnet
|
||||
3: common.HexToAddress("0x8cc272396be7583c65bee82cd7b743c69a87287d"), // ropsten
|
||||
}
|
||||
|
||||
var stickerMarketByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0x12824271339304d3a9f7e096e62a2a7e73b4a7e7"), // mainnet
|
||||
3: common.HexToAddress("0x6CC7274aF9cE9572d22DFD8545Fb8c9C9Bcb48AD"), // ropsten
|
||||
}
|
||||
|
||||
var stickerPackByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0x110101156e8F0743948B2A61aFcf3994A8Fb172e"), // mainnet
|
||||
3: common.HexToAddress("0xf852198d0385c4b871e0b91804ecd47c6ba97351"), // ropsten
|
||||
}
|
||||
|
||||
func StickerTypeContractAddress(chainID uint64) (common.Address, error) {
|
||||
addr, exists := stickerTypeByChainID[chainID]
|
||||
if !exists {
|
||||
return *new(common.Address), errorNotAvailableOnChainID
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func StickerMarketContractAddress(chainID uint64) (common.Address, error) {
|
||||
addr, exists := stickerMarketByChainID[chainID]
|
||||
if !exists {
|
||||
return *new(common.Address), errorNotAvailableOnChainID
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func StickerPackContractAddress(chainID uint64) (common.Address, error) {
|
||||
addr, exists := stickerPackByChainID[chainID]
|
||||
if !exists {
|
||||
return *new(common.Address), errorNotAvailableOnChainID
|
||||
}
|
||||
return addr, nil
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
package stickers
|
||||
|
||||
//go:generate abigen -sol contracts.sol -pkg stickers -out contracts.go
|
1
go.mod
1
go.mod
|
@ -73,4 +73,5 @@ require (
|
|||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -1864,6 +1864,8 @@ modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
|
|||
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
|
||||
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||
modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
|
|
@ -760,6 +760,21 @@ func (db *Database) GetMessagesFromContactsOnly() (bool, error) {
|
|||
return result, err
|
||||
}
|
||||
|
||||
func (db *Database) GetInstalledStickerPacks() (rst *json.RawMessage, err error) {
|
||||
err = db.db.QueryRow("SELECT stickers_packs_installed FROM settings WHERE synthetic_id = 'id'").Scan(&rst)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *Database) GetPendingStickerPacks() (rst *json.RawMessage, err error) {
|
||||
err = db.db.QueryRow("SELECT stickers_packs_pending FROM settings WHERE synthetic_id = 'id'").Scan(&rst)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *Database) GetRecentStickers() (rst *json.RawMessage, err error) {
|
||||
err = db.db.QueryRow("SELECT stickers_recent_stickers FROM settings WHERE synthetic_id = 'id'").Scan(&rst)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *Database) GetWalletAddress() (rst types.Address, err error) {
|
||||
err = db.db.QueryRow("SELECT address FROM accounts WHERE wallet = 1").Scan(&rst)
|
||||
return
|
||||
|
|
|
@ -42,6 +42,7 @@ import (
|
|||
"github.com/status-im/status-go/services/rpcfilters"
|
||||
"github.com/status-im/status-go/services/rpcstats"
|
||||
"github.com/status-im/status-go/services/status"
|
||||
"github.com/status-im/status-go/services/stickers"
|
||||
"github.com/status-im/status-go/services/subscriptions"
|
||||
"github.com/status-im/status-go/services/wakuext"
|
||||
"github.com/status-im/status-go/services/wakuv2ext"
|
||||
|
@ -113,6 +114,7 @@ type StatusNode struct {
|
|||
wakuV2ExtSrvc *wakuv2ext.Service
|
||||
ensSrvc *ens.Service
|
||||
gifSrvc *gif.Service
|
||||
stickersSrvc *stickers.Service
|
||||
}
|
||||
|
||||
// New makes new instance of StatusNode.
|
||||
|
@ -422,6 +424,7 @@ func (n *StatusNode) stop() error {
|
|||
n.wakuV2Srvc = nil
|
||||
n.wakuV2ExtSrvc = nil
|
||||
n.ensSrvc = nil
|
||||
n.stickersSrvc = nil
|
||||
n.publicMethods = make(map[string]bool)
|
||||
|
||||
return nil
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"github.com/status-im/status-go/services/rpcfilters"
|
||||
"github.com/status-im/status-go/services/rpcstats"
|
||||
"github.com/status-im/status-go/services/status"
|
||||
"github.com/status-im/status-go/services/stickers"
|
||||
"github.com/status-im/status-go/services/subscriptions"
|
||||
"github.com/status-im/status-go/services/wakuext"
|
||||
"github.com/status-im/status-go/services/wakuv2ext"
|
||||
|
@ -66,6 +67,7 @@ func (b *StatusNode) initServices(config *params.NodeConfig) error {
|
|||
services = append(services, b.personalService())
|
||||
services = append(services, b.statusPublicService())
|
||||
services = append(services, b.ensService())
|
||||
services = append(services, b.stickersService())
|
||||
services = appendIf(config.EnableNTPSync, services, b.timeSource())
|
||||
services = appendIf(b.appDB != nil && b.multiaccountsDB != nil, services, b.accountsService(accountsFeed))
|
||||
services = appendIf(config.BrowsersConfig.Enabled, services, b.browsersService())
|
||||
|
@ -370,6 +372,13 @@ func (b *StatusNode) ensService() *ens.Service {
|
|||
return b.ensSrvc
|
||||
}
|
||||
|
||||
func (b *StatusNode) stickersService() *stickers.Service {
|
||||
if b.stickersSrvc == nil {
|
||||
b.stickersSrvc = stickers.NewService(b.appDB, b.rpcClient, b.gethAccountManager, b.rpcFiltersSrvc, b.config)
|
||||
}
|
||||
return b.stickersSrvc
|
||||
}
|
||||
|
||||
func (b *StatusNode) gifService() *gif.Service {
|
||||
if b.gifSrvc == nil {
|
||||
b.gifSrvc = gif.NewService(accounts.NewDB(b.appDB))
|
||||
|
|
|
@ -21,19 +21,20 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
ethTypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/contracts"
|
||||
"github.com/status-im/status-go/contracts/registrar"
|
||||
"github.com/status-im/status-go/contracts/resolver"
|
||||
"github.com/status-im/status-go/contracts/snt"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/services/ens/erc20"
|
||||
"github.com/status-im/status-go/services/ens/registrar"
|
||||
"github.com/status-im/status-go/services/ens/resolver"
|
||||
"github.com/status-im/status-go/services/rpcfilters"
|
||||
"github.com/status-im/status-go/transactions"
|
||||
)
|
||||
|
||||
func NewAPI(rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, config *params.NodeConfig) *API {
|
||||
return &API{
|
||||
contractMaker: &contractMaker{
|
||||
contractMaker: &contracts.ContractMaker{
|
||||
RPCClient: rpcClient,
|
||||
},
|
||||
accountsManager: accountsManager,
|
||||
|
@ -49,7 +50,7 @@ type uri struct {
|
|||
}
|
||||
|
||||
type API struct {
|
||||
contractMaker *contractMaker
|
||||
contractMaker *contracts.ContractMaker
|
||||
accountsManager *account.GethManager
|
||||
rpcFiltersSrvc *rpcfilters.Service
|
||||
config *params.NodeConfig
|
||||
|
@ -61,7 +62,7 @@ func (api *API) Resolver(ctx context.Context, chainID uint64, username string) (
|
|||
return nil, err
|
||||
}
|
||||
|
||||
registry, err := api.contractMaker.newRegistry(chainID)
|
||||
registry, err := api.contractMaker.NewRegistry(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -81,7 +82,7 @@ func (api *API) OwnerOf(ctx context.Context, chainID uint64, username string) (*
|
|||
return nil, err
|
||||
}
|
||||
|
||||
registry, err := api.contractMaker.newRegistry(chainID)
|
||||
registry, err := api.contractMaker.NewRegistry(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -106,7 +107,7 @@ func (api *API) ContentHash(ctx context.Context, chainID uint64, username string
|
|||
return nil, err
|
||||
}
|
||||
|
||||
resolver, err := api.contractMaker.newPublicResolver(chainID, resolverAddress)
|
||||
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -131,7 +132,7 @@ func (api *API) PublicKeyOf(ctx context.Context, chainID uint64, username string
|
|||
return "", err
|
||||
}
|
||||
|
||||
resolver, err := api.contractMaker.newPublicResolver(chainID, resolverAddress)
|
||||
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -155,7 +156,7 @@ func (api *API) AddressOf(ctx context.Context, chainID uint64, username string)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
resolver, err := api.contractMaker.newPublicResolver(chainID, resolverAddress)
|
||||
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -170,7 +171,7 @@ func (api *API) AddressOf(ctx context.Context, chainID uint64, username string)
|
|||
}
|
||||
|
||||
func (api *API) ExpireAt(ctx context.Context, chainID uint64, username string) (string, error) {
|
||||
registrar, err := api.contractMaker.newUsernameRegistrar(chainID)
|
||||
registrar, err := api.contractMaker.NewUsernameRegistrar(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -185,7 +186,7 @@ func (api *API) ExpireAt(ctx context.Context, chainID uint64, username string) (
|
|||
}
|
||||
|
||||
func (api *API) Price(ctx context.Context, chainID uint64) (string, error) {
|
||||
registrar, err := api.contractMaker.newUsernameRegistrar(chainID)
|
||||
registrar, err := api.contractMaker.NewUsernameRegistrar(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -211,7 +212,7 @@ func (api *API) getSigner(chainID uint64, from types.Address, password string) b
|
|||
}
|
||||
|
||||
func (api *API) Release(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, password string, username string) (string, error) {
|
||||
registrar, err := api.contractMaker.newUsernameRegistrar(chainID)
|
||||
registrar, err := api.contractMaker.NewUsernameRegistrar(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -242,7 +243,11 @@ func (api *API) ReleaseEstimate(ctx context.Context, chainID uint64, txArgs tran
|
|||
return 0, err
|
||||
}
|
||||
|
||||
registrarAddress := usernameRegistrarsByChainID[chainID]
|
||||
registrarAddress, err := registrar.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ethClient.EstimateGas(ctx, ethereum.CallMsg{
|
||||
From: common.Address(txArgs.From),
|
||||
To: ®istrarAddress,
|
||||
|
@ -252,7 +257,7 @@ func (api *API) ReleaseEstimate(ctx context.Context, chainID uint64, txArgs tran
|
|||
}
|
||||
|
||||
func (api *API) Register(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, password string, username string, pubkey string) (string, error) {
|
||||
snt, err := api.contractMaker.newSNT(chainID)
|
||||
snt, err := api.contractMaker.NewSNT(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -275,10 +280,15 @@ func (api *API) Register(ctx context.Context, chainID uint64, txArgs transaction
|
|||
return "", err
|
||||
}
|
||||
|
||||
registrarAddress, err := registrar.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
txOpts := txArgs.ToTransactOpts(api.getSigner(chainID, txArgs.From, password))
|
||||
tx, err := snt.ApproveAndCall(
|
||||
txOpts,
|
||||
usernameRegistrarsByChainID[chainID],
|
||||
registrarAddress,
|
||||
price,
|
||||
extraData,
|
||||
)
|
||||
|
@ -310,21 +320,28 @@ func (api *API) RegisterPrepareTxCallMsg(ctx context.Context, chainID uint64, tx
|
|||
return ethereum.CallMsg{}, err
|
||||
}
|
||||
|
||||
sntABI, err := abi.JSON(strings.NewReader(erc20.SNTABI))
|
||||
sntABI, err := abi.JSON(strings.NewReader(snt.SNTABI))
|
||||
if err != nil {
|
||||
return ethereum.CallMsg{}, err
|
||||
}
|
||||
|
||||
data, err := sntABI.Pack("approveAndCall", usernameRegistrarsByChainID[chainID], price, extraData)
|
||||
registrarAddress, err := registrar.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return ethereum.CallMsg{}, err
|
||||
}
|
||||
|
||||
contractAddress := sntByChainID[chainID]
|
||||
data, err := sntABI.Pack("approveAndCall", registrarAddress, price, extraData)
|
||||
if err != nil {
|
||||
return ethereum.CallMsg{}, err
|
||||
}
|
||||
|
||||
sntAddress, err := snt.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return ethereum.CallMsg{}, err
|
||||
}
|
||||
return ethereum.CallMsg{
|
||||
From: common.Address(txArgs.From),
|
||||
To: &contractAddress,
|
||||
To: &sntAddress,
|
||||
Value: big.NewInt(0),
|
||||
Data: data,
|
||||
}, nil
|
||||
|
@ -364,7 +381,7 @@ func (api *API) SetPubKey(ctx context.Context, chainID uint64, txArgs transactio
|
|||
return "", err
|
||||
}
|
||||
|
||||
resolver, err := api.contractMaker.newPublicResolver(chainID, resolverAddress)
|
||||
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
package ens
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/services/ens/erc20"
|
||||
"github.com/status-im/status-go/services/ens/registrar"
|
||||
"github.com/status-im/status-go/services/ens/resolver"
|
||||
)
|
||||
|
||||
var errorNotAvailableOnChainID = errors.New("not available for chainID")
|
||||
|
||||
var resolversByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // mainnet
|
||||
3: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // ropsten
|
||||
}
|
||||
|
||||
var usernameRegistrarsByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0xDB5ac1a559b02E12F29fC0eC0e37Be8E046DEF49"), // mainnet
|
||||
3: common.HexToAddress("0xdaae165beb8c06e0b7613168138ebba774aff071"), // ropsten
|
||||
}
|
||||
|
||||
var sntByChainID = map[uint64]common.Address{
|
||||
1: common.HexToAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e"), // mainnet
|
||||
3: common.HexToAddress("0xc55cf4b03948d7ebc8b9e8bad92643703811d162"), // ropsten
|
||||
}
|
||||
|
||||
type contractMaker struct {
|
||||
RPCClient *rpc.Client
|
||||
}
|
||||
|
||||
func (c *contractMaker) newRegistry(chainID uint64) (*resolver.ENSRegistryWithFallback, error) {
|
||||
if _, ok := resolversByChainID[chainID]; !ok {
|
||||
return nil, errorNotAvailableOnChainID
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resolver.NewENSRegistryWithFallback(
|
||||
resolversByChainID[chainID],
|
||||
backend,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *contractMaker) newPublicResolver(chainID uint64, resolverAddress *common.Address) (*resolver.PublicResolver, error) {
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resolver.NewPublicResolver(*resolverAddress, backend)
|
||||
}
|
||||
|
||||
func (c *contractMaker) newUsernameRegistrar(chainID uint64) (*registrar.UsernameRegistrar, error) {
|
||||
if _, ok := usernameRegistrarsByChainID[chainID]; !ok {
|
||||
return nil, errorNotAvailableOnChainID
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return registrar.NewUsernameRegistrar(
|
||||
usernameRegistrarsByChainID[chainID],
|
||||
backend,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *contractMaker) newSNT(chainID uint64) (*erc20.SNT, error) {
|
||||
if _, ok := sntByChainID[chainID]; !ok {
|
||||
return nil, errorNotAvailableOnChainID
|
||||
}
|
||||
|
||||
backend, err := c.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return erc20.NewSNT(sntByChainID[chainID], backend)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package erc20
|
||||
|
||||
//go:generate abigen -sol erc20.sol -pkg erc20 -out erc20.go
|
|
@ -0,0 +1,540 @@
|
|||
package stickers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/multiformats/go-multibase"
|
||||
"github.com/wealdtech/go-multicodec"
|
||||
"olympos.io/encoding/edn"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/contracts"
|
||||
"github.com/status-im/status-go/contracts/stickers"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/services/rpcfilters"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
)
|
||||
|
||||
const ipfsGateway = ".ipfs.cf-ipfs.com"
|
||||
|
||||
// ConnectionType constants
|
||||
type stickerStatus int
|
||||
|
||||
const (
|
||||
statusInstalled stickerStatus = iota
|
||||
statusPurchased
|
||||
statusPending
|
||||
)
|
||||
|
||||
type API struct {
|
||||
contractMaker *contracts.ContractMaker
|
||||
accountsManager *account.GethManager
|
||||
accountsDB *accounts.Database
|
||||
rpcFiltersSrvc *rpcfilters.Service
|
||||
config *params.NodeConfig
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type Sticker struct {
|
||||
PackID *bigint.BigInt `json:"packID"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
}
|
||||
|
||||
type StickerPack struct {
|
||||
ID *bigint.BigInt `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
Owner common.Address `json:"owner"`
|
||||
Price *bigint.BigInt `json:"price"`
|
||||
Preview string `json:"preview"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Stickers []Sticker `json:"stickers"`
|
||||
|
||||
Status stickerStatus `json:"status"`
|
||||
}
|
||||
|
||||
type ednSticker struct {
|
||||
Hash string
|
||||
}
|
||||
|
||||
type ednStickerPack struct {
|
||||
Name string
|
||||
Author string
|
||||
Thumbnail string
|
||||
Preview string
|
||||
Stickers []ednSticker
|
||||
}
|
||||
type ednStickerPackInfo struct {
|
||||
Meta ednStickerPack
|
||||
}
|
||||
|
||||
func NewAPI(ctx context.Context, appDB *sql.DB, rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, config *params.NodeConfig) *API {
|
||||
return &API{
|
||||
contractMaker: &contracts.ContractMaker{
|
||||
RPCClient: rpcClient,
|
||||
},
|
||||
accountsManager: accountsManager,
|
||||
accountsDB: accounts.NewDB(appDB),
|
||||
rpcFiltersSrvc: rpcFiltersSrvc,
|
||||
config: config,
|
||||
ctx: ctx,
|
||||
client: &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) Market(chainID uint64) ([]StickerPack, error) {
|
||||
// TODO: eventually this should be changed to include pagination
|
||||
|
||||
accounts, err := api.accountsDB.GetAccounts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allStickerPacks, err := api.getContractPacks(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
purchasedPacks := make(map[uint]struct{})
|
||||
|
||||
purchasedPackChan := make(chan *big.Int)
|
||||
errChan := make(chan error)
|
||||
doneChan := make(chan struct{}, 1)
|
||||
go api.getAccountsPurchasedPack(chainID, accounts, purchasedPackChan, errChan, doneChan)
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case packID := <-purchasedPackChan:
|
||||
if packID != nil {
|
||||
purchasedPacks[uint(packID.Uint64())] = struct{}{}
|
||||
}
|
||||
|
||||
case <-doneChan:
|
||||
// TODO: add an attribute to indicate if the sticker pack
|
||||
// is bought, but the transaction is still pending confirmation.
|
||||
var result []StickerPack
|
||||
for _, pack := range allStickerPacks {
|
||||
packID := uint(pack.ID.Uint64())
|
||||
_, isPurchased := purchasedPacks[packID]
|
||||
if isPurchased {
|
||||
pack.Status = statusPurchased
|
||||
}
|
||||
result = append(result, pack)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) execTokenPackID(chainID uint64, tokenIDs []*big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
|
||||
defer close(doneChan)
|
||||
defer close(errChan)
|
||||
defer close(resultChan)
|
||||
|
||||
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(tokenIDs))
|
||||
for _, tokenID := range tokenIDs {
|
||||
go func(tokenID *big.Int) {
|
||||
defer wg.Done()
|
||||
packID, err := stickerPack.TokenPackId(callOpts, tokenID)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
resultChan <- packID
|
||||
}(tokenID)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (api *API) getTokenPackIDs(chainID uint64, tokenIDs []*big.Int) ([]*big.Int, error) {
|
||||
tokenPackIDChan := make(chan *big.Int)
|
||||
errChan := make(chan error)
|
||||
doneChan := make(chan struct{}, 1)
|
||||
|
||||
go api.execTokenPackID(chainID, tokenIDs, tokenPackIDChan, errChan, doneChan)
|
||||
|
||||
var tokenPackIDs []*big.Int
|
||||
for {
|
||||
select {
|
||||
case <-doneChan:
|
||||
return tokenPackIDs, nil
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case t := <-tokenPackIDChan:
|
||||
if t != nil {
|
||||
tokenPackIDs = append(tokenPackIDs, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) getPurchasedPackIDs(chainID uint64, account types.Address) ([]*big.Int, error) {
|
||||
// TODO: this should be replaced in the future by something like TheGraph to reduce the number of requests to infura
|
||||
|
||||
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
|
||||
|
||||
balance, err := stickerPack.BalanceOf(callOpts, common.Address(account))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenIDs, err := api.getTokenOwnerOfIndex(chainID, account, balance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return api.getTokenPackIDs(chainID, tokenIDs)
|
||||
}
|
||||
|
||||
func hashToURL(hash []byte) (string, error) {
|
||||
// contract response includes a contenthash, which needs to be decoded to reveal
|
||||
// an IPFS identifier. Once decoded, download the content from IPFS. This content
|
||||
// is in EDN format, ie https://ipfs.infura.io/ipfs/QmWVVLwVKCwkVNjYJrRzQWREVvEk917PhbHYAUhA1gECTM
|
||||
// and it also needs to be decoded in to a nim type
|
||||
|
||||
data, codec, err := multicodec.RemoveCodec(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
codecName, err := multicodec.Name(codec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if codecName != "ipfs-ns" {
|
||||
return "", errors.New("codecName is not ipfs-ns")
|
||||
}
|
||||
|
||||
thisCID, err := cid.Parse(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
str, err := thisCID.StringOfBase(multibase.Base32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "https://" + str + ipfsGateway, nil
|
||||
}
|
||||
|
||||
func (api *API) fetchStickerPacks(chainID uint64, resultChan chan<- *StickerPack, errChan chan<- error, doneChan chan<- struct{}) {
|
||||
defer close(doneChan)
|
||||
defer close(errChan)
|
||||
defer close(resultChan)
|
||||
|
||||
installedPacks, err := api.Installed()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
pendingPacks, err := api.pendingStickerPacks()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
stickerType, err := api.contractMaker.NewStickerType(chainID)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
|
||||
|
||||
numPacks, err := stickerType.PackCount(callOpts)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(int(numPacks.Int64()))
|
||||
for i := uint64(0); i < numPacks.Uint64(); i++ {
|
||||
go func(i uint64) {
|
||||
defer wg.Done()
|
||||
|
||||
packID := new(big.Int).SetUint64(i)
|
||||
|
||||
_, exists := installedPacks[uint(i)]
|
||||
if exists {
|
||||
return // We already have the sticker pack data, no need to query it
|
||||
}
|
||||
|
||||
_, exists = pendingPacks[uint(i)]
|
||||
if exists {
|
||||
return // We already have the sticker pack data, no need to query it
|
||||
}
|
||||
|
||||
stickerPack, err := api.fetchPackData(stickerType, packID, true)
|
||||
if err != nil {
|
||||
log.Warn("Could not retrieve stickerpack data", "packID", packID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
resultChan <- stickerPack
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (api *API) fetchPackData(stickerType *stickers.StickerType, packID *big.Int, translateHashes bool) (*StickerPack, error) {
|
||||
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
|
||||
|
||||
packData, err := stickerType.GetPackData(callOpts, packID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packDetailsURL, err := hashToURL(packData.Contenthash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stickerPack := &StickerPack{
|
||||
ID: &bigint.BigInt{Int: packID},
|
||||
Owner: packData.Owner,
|
||||
Price: &bigint.BigInt{Int: packData.Price},
|
||||
}
|
||||
|
||||
err = api.downloadIPFSData(stickerPack, packDetailsURL, translateHashes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stickerPack, nil
|
||||
}
|
||||
|
||||
func (api *API) downloadIPFSData(stickerPack *StickerPack, packDetailsURL string, translateHashes bool) error {
|
||||
// This can be improved by adding a cache using packDetailsURL as key
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, packDetailsURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := api.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Error("failed to close the stickerpack request body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return populateStickerPackAttributes(stickerPack, body, translateHashes)
|
||||
}
|
||||
|
||||
func populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error {
|
||||
var stickerpackIPFSInfo ednStickerPackInfo
|
||||
err := edn.Unmarshal(ednSource, &stickerpackIPFSInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stickerPack.Author = stickerpackIPFSInfo.Meta.Author
|
||||
stickerPack.Name = stickerpackIPFSInfo.Meta.Name
|
||||
|
||||
if translateHashes {
|
||||
stickerPack.Preview, err = decodeStringHash(stickerpackIPFSInfo.Meta.Preview)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stickerPack.Thumbnail, err = decodeStringHash(stickerpackIPFSInfo.Meta.Thumbnail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
stickerPack.Preview = stickerpackIPFSInfo.Meta.Preview
|
||||
stickerPack.Thumbnail = stickerpackIPFSInfo.Meta.Thumbnail
|
||||
}
|
||||
|
||||
for _, s := range stickerpackIPFSInfo.Meta.Stickers {
|
||||
url := ""
|
||||
if translateHashes {
|
||||
hash, err := hexutil.Decode("0x" + s.Hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url, err = hashToURL(hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
stickerPack.Stickers = append(stickerPack.Stickers, Sticker{
|
||||
PackID: stickerPack.ID,
|
||||
URL: url,
|
||||
Hash: s.Hash,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeStringHash(input string) (string, error) {
|
||||
hash, err := hexutil.Decode("0x" + input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url, err := hashToURL(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (api *API) getContractPacks(chainID uint64) ([]StickerPack, error) {
|
||||
stickerPackChan := make(chan *StickerPack)
|
||||
errChan := make(chan error)
|
||||
doneChan := make(chan struct{}, 1)
|
||||
|
||||
go api.fetchStickerPacks(chainID, stickerPackChan, errChan, doneChan)
|
||||
|
||||
var packs []StickerPack
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-doneChan:
|
||||
return packs, nil
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case pack := <-stickerPackChan:
|
||||
if pack != nil {
|
||||
packs = append(packs, *pack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) getAccountsPurchasedPack(chainID uint64, accs []accounts.Account, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
|
||||
defer close(doneChan)
|
||||
defer close(errChan)
|
||||
defer close(resultChan)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(accs))
|
||||
for _, account := range accs {
|
||||
go func(acc accounts.Account) {
|
||||
defer wg.Done()
|
||||
packs, err := api.getPurchasedPackIDs(chainID, acc.Address)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range packs {
|
||||
resultChan <- p
|
||||
}
|
||||
}(account)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (api *API) execTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
|
||||
defer close(doneChan)
|
||||
defer close(errChan)
|
||||
defer close(resultChan)
|
||||
|
||||
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(int(balance.Int64()))
|
||||
for i := uint64(0); i < balance.Uint64(); i++ {
|
||||
go func(i uint64) {
|
||||
defer wg.Done()
|
||||
tokenID, err := stickerPack.TokenOfOwnerByIndex(callOpts, common.Address(account), new(big.Int).SetUint64(i))
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
resultChan <- tokenID
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (api *API) getTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int) ([]*big.Int, error) {
|
||||
tokenIDChan := make(chan *big.Int)
|
||||
errChan := make(chan error)
|
||||
doneChan := make(chan struct{}, 1)
|
||||
|
||||
go api.execTokenOwnerOfIndex(chainID, account, balance, tokenIDChan, errChan, doneChan)
|
||||
|
||||
var tokenIDs []*big.Int
|
||||
for {
|
||||
select {
|
||||
case <-doneChan:
|
||||
return tokenIDs, nil
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case tokenID := <-tokenIDChan:
|
||||
if tokenID != nil {
|
||||
tokenIDs = append(tokenIDs, tokenID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package stickers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
)
|
||||
|
||||
func (api *API) Install(chainID uint64, packID uint64) error {
|
||||
installedPacks, err := api.installedStickerPacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, exists := installedPacks[uint(packID)]; exists {
|
||||
return errors.New("sticker pack is already installed")
|
||||
}
|
||||
|
||||
// TODO: this does not validate if the pack is purchased. Should it?
|
||||
|
||||
stickerType, err := api.contractMaker.NewStickerType(chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stickerPack, err := api.fetchPackData(stickerType, new(big.Int).SetUint64(packID), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installedPacks[uint(packID)] = *stickerPack
|
||||
|
||||
err = api.accountsDB.SaveSetting("stickers/packs-installed", installedPacks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) installedStickerPacks() (map[uint]StickerPack, error) {
|
||||
stickerPacks := make(map[uint]StickerPack)
|
||||
|
||||
installedStickersJSON, err := api.accountsDB.GetInstalledStickerPacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if installedStickersJSON == nil {
|
||||
return stickerPacks, nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal(*installedStickersJSON, &stickerPacks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stickerPacks, nil
|
||||
}
|
||||
|
||||
func (api *API) Installed() (map[uint]StickerPack, error) {
|
||||
stickerPacks, err := api.installedStickerPacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for packID, stickerPack := range stickerPacks {
|
||||
stickerPack.Status = statusInstalled
|
||||
|
||||
stickerPack.Preview, err = decodeStringHash(stickerPack.Preview)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stickerPack.Thumbnail, err = decodeStringHash(stickerPack.Thumbnail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, sticker := range stickerPack.Stickers {
|
||||
sticker.URL, err = decodeStringHash(sticker.Hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stickerPack.Stickers[i] = sticker
|
||||
}
|
||||
|
||||
stickerPacks[packID] = stickerPack
|
||||
}
|
||||
|
||||
return stickerPacks, nil
|
||||
}
|
||||
|
||||
func (api *API) Uninstall(packID uint64) error {
|
||||
installedPacks, err := api.installedStickerPacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, exists := installedPacks[uint(packID)]; !exists {
|
||||
return errors.New("sticker pack is not installed")
|
||||
}
|
||||
|
||||
delete(installedPacks, uint(packID))
|
||||
|
||||
err = api.accountsDB.SaveSetting("stickers/packs-installed", installedPacks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Removing uninstalled pack from recent stickers
|
||||
|
||||
recentStickers, err := api.recentStickers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pID := &bigint.BigInt{Int: new(big.Int).SetUint64(packID)}
|
||||
idx := -1
|
||||
for i, r := range recentStickers {
|
||||
if r.PackID.Cmp(pID.Int) == 0 {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx > -1 {
|
||||
var newRecentStickers []Sticker
|
||||
newRecentStickers = append(newRecentStickers, recentStickers[:idx]...)
|
||||
if idx != len(recentStickers)-1 {
|
||||
newRecentStickers = append(newRecentStickers, recentStickers[idx+1:]...)
|
||||
}
|
||||
return api.accountsDB.SaveSetting("stickers/recent-stickers", newRecentStickers)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package stickers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func (api *API) AddPending(chainID uint64, packID uint64) error {
|
||||
pendingPacks, err := api.pendingStickerPacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, exists := pendingPacks[uint(packID)]; exists {
|
||||
return errors.New("sticker pack is already pending")
|
||||
}
|
||||
|
||||
stickerType, err := api.contractMaker.NewStickerType(chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stickerPack, err := api.fetchPackData(stickerType, new(big.Int).SetUint64(packID), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pendingPacks[uint(packID)] = *stickerPack
|
||||
|
||||
return api.accountsDB.SaveSetting("stickers/packs-pending", pendingPacks)
|
||||
}
|
||||
|
||||
func (api *API) pendingStickerPacks() (map[uint]StickerPack, error) {
|
||||
stickerPacks := make(map[uint]StickerPack)
|
||||
|
||||
pendingStickersJSON, err := api.accountsDB.GetPendingStickerPacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pendingStickersJSON == nil {
|
||||
return stickerPacks, nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal(*pendingStickersJSON, &stickerPacks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stickerPacks, nil
|
||||
}
|
||||
|
||||
func (api *API) Pending() (map[uint]StickerPack, error) {
|
||||
stickerPacks, err := api.pendingStickerPacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for packID, stickerPack := range stickerPacks {
|
||||
stickerPack.Status = statusPending
|
||||
stickerPacks[packID] = stickerPack
|
||||
}
|
||||
|
||||
return stickerPacks, nil
|
||||
}
|
||||
|
||||
func (api *API) RemovePending(packID uint64) error {
|
||||
pendingPacks, err := api.pendingStickerPacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, exists := pendingPacks[uint(packID)]; !exists {
|
||||
return errors.New("sticker pack is not pending")
|
||||
}
|
||||
|
||||
delete(pendingPacks, uint(packID))
|
||||
|
||||
return api.accountsDB.SaveSetting("stickers/packs-pending", pendingPacks)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package stickers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const maxNumberRecentStickers = 24
|
||||
|
||||
func (api *API) recentStickers() ([]Sticker, error) {
|
||||
var recentStickersList []Sticker
|
||||
|
||||
recentStickersJSON, err := api.accountsDB.GetRecentStickers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if recentStickersJSON == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal(*recentStickersJSON, &recentStickersList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recentStickersList, nil
|
||||
}
|
||||
|
||||
func (api *API) Recent() ([]Sticker, error) {
|
||||
recentStickersList, err := api.recentStickers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, sticker := range recentStickersList {
|
||||
sticker.URL, err = decodeStringHash(sticker.Hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recentStickersList[i] = sticker
|
||||
}
|
||||
|
||||
return recentStickersList, nil
|
||||
}
|
||||
|
||||
func (api *API) AddRecent(sticker Sticker) error {
|
||||
recentStickersList, err := api.recentStickers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove duplicated
|
||||
idx := -1
|
||||
for i, currSticker := range recentStickersList {
|
||||
if currSticker.PackID.Cmp(sticker.PackID.Int) == 0 {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
if idx > -1 {
|
||||
recentStickersList = append(recentStickersList[:idx], recentStickersList[idx+1:]...)
|
||||
}
|
||||
|
||||
sticker.URL = ""
|
||||
|
||||
if len(recentStickersList) >= maxNumberRecentStickers {
|
||||
recentStickersList = append([]Sticker{sticker}, recentStickersList[:maxNumberRecentStickers-1]...)
|
||||
} else {
|
||||
recentStickersList = append([]Sticker{sticker}, recentStickersList...)
|
||||
}
|
||||
|
||||
return api.accountsDB.SaveSetting("stickers/recent-stickers", recentStickersList)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package stickers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
ethRpc "github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/services/rpcfilters"
|
||||
)
|
||||
|
||||
// NewService initializes service instance.
|
||||
func NewService(appDB *sql.DB, rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, config *params.NodeConfig) *Service {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Service{
|
||||
appDB: appDB,
|
||||
rpcClient: rpcClient,
|
||||
accountsManager: accountsManager,
|
||||
rpcFiltersSrvc: rpcFiltersSrvc,
|
||||
config: config,
|
||||
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Service is a browsers service.
|
||||
type Service struct {
|
||||
appDB *sql.DB
|
||||
rpcClient *rpc.Client
|
||||
accountsManager *account.GethManager
|
||||
rpcFiltersSrvc *rpcfilters.Service
|
||||
config *params.NodeConfig
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Start a service.
|
||||
func (s *Service) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop a service.
|
||||
func (s *Service) Stop() error {
|
||||
s.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
// APIs returns list of available RPC APIs.
|
||||
func (s *Service) APIs() []ethRpc.API {
|
||||
return []ethRpc.API{
|
||||
{
|
||||
Namespace: "stickers",
|
||||
Version: "0.1.0",
|
||||
Service: NewAPI(s.ctx, s.appDB, s.rpcClient, s.accountsManager, s.rpcFiltersSrvc, s.config),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Protocols returns list of p2p protocols.
|
||||
func (s *Service) Protocols() []p2p.Protocol {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package stickers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethTypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/status-im/status-go/contracts/snt"
|
||||
"github.com/status-im/status-go/contracts/stickers"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/transactions"
|
||||
)
|
||||
|
||||
func (api *API) getSigner(chainID uint64, from types.Address, password string) bind.SignerFn {
|
||||
return func(addr common.Address, tx *ethTypes.Transaction) (*ethTypes.Transaction, error) {
|
||||
selectedAccount, err := api.accountsManager.VerifyAccountPassword(api.config.KeyStoreDir, from.Hex(), password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := ethTypes.NewLondonSigner(new(big.Int).SetUint64(chainID))
|
||||
return ethTypes.SignTx(tx, s, selectedAccount.PrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) Buy(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, packID *big.Int, password string) (string, error) {
|
||||
err := api.AddPending(chainID, packID.Uint64())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
snt, err := api.contractMaker.NewSNT(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stickerType, err := api.contractMaker.NewStickerType(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
|
||||
|
||||
packInfo, err := stickerType.GetPackData(callOpts, packID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stickerMarketABI, err := abi.JSON(strings.NewReader(stickers.StickerMarketABI))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
extraData, err := stickerMarketABI.Pack("buyToken", packID, packInfo.Price)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stickerMarketAddress, err := stickers.StickerMarketContractAddress(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
txOpts := txArgs.ToTransactOpts(api.getSigner(chainID, txArgs.From, password))
|
||||
tx, err := snt.ApproveAndCall(
|
||||
txOpts,
|
||||
stickerMarketAddress,
|
||||
packInfo.Price,
|
||||
extraData,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO: track pending transaction (do this in ENS service too)
|
||||
|
||||
go api.rpcFiltersSrvc.TriggerTransactionSentToUpstreamEvent(types.Hash(tx.Hash()))
|
||||
return tx.Hash().String(), nil
|
||||
}
|
||||
|
||||
func (api *API) BuyEstimate(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, packID *big.Int) (uint64, error) {
|
||||
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
|
||||
|
||||
stickerType, err := api.contractMaker.NewStickerType(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
packInfo, err := stickerType.GetPackData(callOpts, packID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
stickerMarketABI, err := abi.JSON(strings.NewReader(stickers.StickerMarketABI))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extraData, err := stickerMarketABI.Pack("buyToken", packID, packInfo.Price)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
sntABI, err := abi.JSON(strings.NewReader(snt.SNTABI))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
stickerMarketAddress, err := stickers.StickerMarketContractAddress(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
data, err := sntABI.Pack("approveAndCall", stickerMarketAddress, packInfo.Price, extraData)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ethClient, err := api.contractMaker.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
sntAddress, err := snt.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ethClient.EstimateGas(ctx, ethereum.CallMsg{
|
||||
From: common.Address(txArgs.From),
|
||||
To: &sntAddress,
|
||||
Value: big.NewInt(0),
|
||||
Data: data,
|
||||
})
|
||||
}
|
|
@ -12,9 +12,9 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/contracts/ierc20"
|
||||
"github.com/status-im/status-go/services/wallet/async"
|
||||
"github.com/status-im/status-go/services/wallet/chain"
|
||||
"github.com/status-im/status-go/services/wallet/ierc20"
|
||||
)
|
||||
|
||||
var requestTimeout = 20 * time.Second
|
||||
|
|
|
@ -687,3 +687,5 @@ gopkg.in/natefinch/npipe.v2
|
|||
gopkg.in/urfave/cli.v1
|
||||
# gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gopkg.in/yaml.v3
|
||||
# olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3
|
||||
olympos.io/encoding/edn
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.5
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2015, The Go Authors, Jean Niklas L'orange
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
* Neither the name of Google Inc., the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,116 @@
|
|||
# Go implementation of EDN, extensible data notation
|
||||
|
||||
[![GoDoc](https://godoc.org/olympos.io/encoding/edn?status.svg)](https://godoc.org/olympos.io/encoding/edn)
|
||||
|
||||
go-edn is a Golang library to read and write
|
||||
[EDN](https://github.com/edn-format/edn) (extensible data notation), a subset of
|
||||
Clojure used for transferring data between applications, much like JSON or XML.
|
||||
EDN is also a very good language for configuration files, much like a JSON-like
|
||||
version of YAML.
|
||||
|
||||
This library is heavily influenced by the JSON library that ships with Go, and
|
||||
people familiar with that package should know the basics of how this library
|
||||
works. In fact, this should be close to a drop-in replacement for the
|
||||
`encoding/json` package if you only use basic functionality.
|
||||
|
||||
This implementation is complete, stable, and presumably also bug free. This
|
||||
is why you don't see any changes in the repository.
|
||||
|
||||
If you wonder why you should (or should not) use EDN, you can have a look at the
|
||||
[why](docs/why.md) document.
|
||||
|
||||
## Installation and Usage
|
||||
|
||||
The import path for the package is `olympos.io/encoding/edn`
|
||||
|
||||
To install it, run:
|
||||
|
||||
```shell
|
||||
go get olympos.io/encoding/edn
|
||||
```
|
||||
|
||||
To use it in your project, you import `olympos.io/encoding/edn` and refer to it as `edn`
|
||||
like this:
|
||||
|
||||
```go
|
||||
import "olympos.io/encoding/edn"
|
||||
|
||||
//...
|
||||
|
||||
edn.DoStuff()
|
||||
```
|
||||
|
||||
The previous import path of this library was `gopkg.in/edn.v1`, which is still
|
||||
permanently supported.
|
||||
|
||||
## Quickstart
|
||||
|
||||
You can follow http://blog.golang.org/json-and-go and replace every occurence of
|
||||
JSON with EDN (and the JSON data with EDN data), and the text makes almost
|
||||
perfect sense. The only caveat is that, since EDN is more general than JSON, go-edn
|
||||
stores arbitrary maps on the form `map[interface{}]interface{}`.
|
||||
|
||||
go-edn also ships with keywords, symbols and tags as types.
|
||||
|
||||
For a longer introduction on how to use the library, see
|
||||
[introduction.md](docs/introduction.md). If you're familiar with the JSON
|
||||
package, then the [API Documentation](https://godoc.org/olympos.io/encoding/edn) might
|
||||
be the only thing you need.
|
||||
|
||||
## Example Usage
|
||||
|
||||
Say you want to describe your pet forum's users as EDN. They have the following
|
||||
types:
|
||||
|
||||
```go
|
||||
type Animal struct {
|
||||
Name string
|
||||
Type string `edn:"kind"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Birthyear int `edn:"born"`
|
||||
Pets []Animal
|
||||
}
|
||||
```
|
||||
|
||||
With go-edn, we can do as follows to read and write these types:
|
||||
|
||||
```go
|
||||
import "olympos.io/encoding/edn"
|
||||
|
||||
//...
|
||||
|
||||
|
||||
func ReturnData() (Person, error) {
|
||||
data := `{:name "Hans",
|
||||
:born 1970,
|
||||
:pets [{:name "Cap'n Jack" :kind "Sparrow"}
|
||||
{:name "Freddy" :kind "Cockatiel"}]}`
|
||||
var user Person
|
||||
err := edn.Unmarshal([]byte(data), &user)
|
||||
// user '==' Person{"Hans", 1970,
|
||||
// []Animal{{"Cap'n Jack", "Sparrow"}, {"Freddy", "Cockatiel"}}}
|
||||
return user, err
|
||||
}
|
||||
```
|
||||
|
||||
If you want to write that user again, just `Marshal` it:
|
||||
|
||||
```go
|
||||
bs, err := edn.Marshal(user)
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
go-edn has no external dependencies, except the default Go library. However, as
|
||||
it depends on `math/big.Float`, go-edn requires Go 1.5 or higher.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2015-2019 Jean Niklas L'orange and [contributors](https://github.com/go-edn/edn/graphs/contributors)
|
||||
|
||||
Distributed under the BSD 3-clause license, which is available in the file
|
||||
LICENSE.
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2015 Jean Niklas L'orange. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
func tokNeedsDelim(t tokenType) bool {
|
||||
switch t {
|
||||
case tokenString, tokenListStart, tokenListEnd, tokenVectorStart,
|
||||
tokenVectorEnd, tokenMapEnd, tokenMapStart, tokenSetStart, tokenDiscard, tokenError:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func delimits(r rune) bool {
|
||||
switch r {
|
||||
case '{', '}', '[', ']', '(', ')', '\\', '"':
|
||||
return true
|
||||
}
|
||||
return isWhitespace(r)
|
||||
}
|
||||
|
||||
// Compact appends to dst a compacted form of the EDN-encoded src. It does not
|
||||
// remove discard values.
|
||||
func Compact(dst *bytes.Buffer, src []byte) error {
|
||||
origLen := dst.Len()
|
||||
var lex lexer
|
||||
lex.reset()
|
||||
buf := bytes.NewBuffer(src)
|
||||
start, pos := 0, 0
|
||||
needsDelim := false
|
||||
prevIgnore := '\uFFFD'
|
||||
r, size, err := buf.ReadRune()
|
||||
for ; err == nil; r, size, err = buf.ReadRune() {
|
||||
ls := lex.state(r)
|
||||
ppos := pos
|
||||
pos += size
|
||||
switch ls {
|
||||
case lexCont:
|
||||
if ppos == start && needsDelim && !delimits(r) {
|
||||
dst.WriteRune(prevIgnore)
|
||||
}
|
||||
continue
|
||||
case lexIgnore:
|
||||
prevIgnore = r
|
||||
start = pos
|
||||
case lexError:
|
||||
dst.Truncate(origLen)
|
||||
return lex.err
|
||||
case lexEnd:
|
||||
// here we might want to discard #_ and the like. Currently we don't.
|
||||
dst.Write(src[start:pos])
|
||||
needsDelim = tokNeedsDelim(lex.token)
|
||||
lex.reset()
|
||||
start = pos
|
||||
case lexEndPrev:
|
||||
dst.Write(src[start:ppos])
|
||||
lex.reset()
|
||||
lss := lex.state(r)
|
||||
needsDelim = tokNeedsDelim(lex.token)
|
||||
switch lss {
|
||||
case lexIgnore:
|
||||
prevIgnore = r
|
||||
start = pos
|
||||
case lexCont:
|
||||
start = ppos
|
||||
case lexEnd:
|
||||
dst.WriteRune(r)
|
||||
lex.reset()
|
||||
start = pos
|
||||
case lexEndPrev:
|
||||
dst.Truncate(origLen)
|
||||
return errInternal
|
||||
case lexError:
|
||||
dst.Truncate(origLen)
|
||||
return lex.err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != io.EOF {
|
||||
return err
|
||||
}
|
||||
ls := lex.eof()
|
||||
switch ls {
|
||||
case lexEnd:
|
||||
dst.Write(src[start:pos])
|
||||
case lexError:
|
||||
dst.Truncate(origLen)
|
||||
return lex.err
|
||||
}
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,142 @@
|
|||
// Copyright 2015 Jean Niklas L'orange. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package edn implements encoding and decoding of EDN values as defined in
|
||||
// https://github.com/edn-format/edn. For a full introduction on how to use
|
||||
// go-edn, see https://github.com/go-edn/edn/blob/v1/docs/introduction.md. Fully
|
||||
// self-contained examples of go-edn can be found at
|
||||
// https://github.com/go-edn/edn/tree/v1/examples.
|
||||
//
|
||||
// Note that the small examples in this package is not checking errors as
|
||||
// persively as you should do when you use this package. This is done because
|
||||
// I'd like the examples to be easily readable and understandable. The bigger
|
||||
// examples provide proper error handling.
|
||||
package edn
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFunc = errors.New("Value is not a function")
|
||||
ErrMismatchArities = errors.New("Function does not have single argument in, two argument out")
|
||||
ErrNotConcrete = errors.New("Value is not a concrete non-function type")
|
||||
ErrTagOverwritten = errors.New("Previous tag implementation was overwritten")
|
||||
)
|
||||
|
||||
var globalTags TagMap
|
||||
|
||||
// A TagMap contains mappings from tag literals to functions and structs that is
|
||||
// used when decoding.
|
||||
type TagMap struct {
|
||||
sync.RWMutex
|
||||
m map[string]reflect.Value
|
||||
}
|
||||
|
||||
var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
// AddTagFn adds fn as a converter function for tagname tags to this TagMap. fn
|
||||
// must have the signature func(T) (U, error), where T is the expected input
|
||||
// type and U is the output type. See Decoder.AddTagFn for examples.
|
||||
func (tm *TagMap) AddTagFn(tagname string, fn interface{}) error {
|
||||
// TODO: check name
|
||||
rfn := reflect.ValueOf(fn)
|
||||
rtyp := rfn.Type()
|
||||
if rtyp.Kind() != reflect.Func {
|
||||
return ErrNotFunc
|
||||
}
|
||||
if rtyp.NumIn() != 1 || rtyp.NumOut() != 2 || !rtyp.Out(1).Implements(errorType) {
|
||||
// ok to have variadic arity?
|
||||
return ErrMismatchArities
|
||||
}
|
||||
return tm.addVal(tagname, rfn)
|
||||
}
|
||||
|
||||
// MustAddTagFn adds fn as a converter function for tagname tags to this TagMap
|
||||
// like AddTagFn, except this function panics if the tag could not be added.
|
||||
func (tm *TagMap) MustAddTagFn(tagname string, fn interface{}) {
|
||||
if err := tm.AddTagFn(tagname, fn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *TagMap) addVal(name string, val reflect.Value) error {
|
||||
tm.Lock()
|
||||
if tm.m == nil {
|
||||
tm.m = map[string]reflect.Value{}
|
||||
}
|
||||
_, ok := tm.m[name]
|
||||
tm.m[name] = val
|
||||
tm.Unlock()
|
||||
if ok {
|
||||
return ErrTagOverwritten
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AddTagFn adds fn as a converter function for tagname tags to the global
|
||||
// TagMap. fn must have the signature func(T) (U, error), where T is the
|
||||
// expected input type and U is the output type. See Decoder.AddTagFn for
|
||||
// examples.
|
||||
func AddTagFn(tagname string, fn interface{}) error {
|
||||
return globalTags.AddTagFn(tagname, fn)
|
||||
}
|
||||
|
||||
// MustAddTagFn adds fn as a converter function for tagname tags to the global
|
||||
// TagMap like AddTagFn, except this function panics if the tag could not be added.
|
||||
func MustAddTagFn(tagname string, fn interface{}) {
|
||||
globalTags.MustAddTagFn(tagname, fn)
|
||||
}
|
||||
|
||||
// AddTagStructs adds the struct as a matching struct for tagname tags to this
|
||||
// TagMap. val can not be a channel, function, interface or an unsafe pointer.
|
||||
// See Decoder.AddTagStruct for examples.
|
||||
func (tm *TagMap) AddTagStruct(tagname string, val interface{}) error {
|
||||
rstruct := reflect.ValueOf(val)
|
||||
switch rstruct.Type().Kind() {
|
||||
case reflect.Invalid, reflect.Chan, reflect.Func, reflect.Interface, reflect.UnsafePointer:
|
||||
return ErrNotConcrete
|
||||
}
|
||||
return tm.addVal(tagname, rstruct)
|
||||
}
|
||||
|
||||
// AddTagStructs adds the struct as a matching struct for tagname tags to the
|
||||
// global TagMap. val can not be a channel, function, interface or an unsafe
|
||||
// pointer. See Decoder.AddTagStruct for examples.
|
||||
func AddTagStruct(tagname string, val interface{}) error {
|
||||
return globalTags.AddTagStruct(tagname, val)
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := AddTagFn("inst", func(s string) (time.Time, error) {
|
||||
return time.Parse(time.RFC3339Nano, s)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = AddTagFn("base64", base64.StdEncoding.DecodeString)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// A MathContext specifies the precision and rounding mode for
|
||||
// `math/big.Float`s when decoding.
|
||||
type MathContext struct {
|
||||
Precision uint
|
||||
Mode big.RoundingMode
|
||||
}
|
||||
|
||||
// The GlobalMathContext is the global MathContext. It is used if no other
|
||||
// context is provided. See MathContext for example usage.
|
||||
var GlobalMathContext = MathContext{
|
||||
Mode: big.ToNearestEven,
|
||||
Precision: 192,
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edn
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// getu4 decodes \uXXXX from the beginning of s, returning the hex value,
|
||||
// or it returns -1.
|
||||
func getu4(s []byte) rune {
|
||||
if len(s) < 6 || s[0] != '\\' || s[1] != 'u' {
|
||||
return -1
|
||||
}
|
||||
r, err := strconv.ParseUint(string(s[2:6]), 16, 64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return rune(r)
|
||||
}
|
||||
|
||||
// indirect walks down v allocating pointers as needed,
|
||||
// until it gets to a non-pointer.
|
||||
// if it encounters an Unmarshaler, indirect stops and returns that.
|
||||
// if decodingNull is true, indirect stops at the last pointer so it can be set to nil.
|
||||
func (d *Decoder) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, reflect.Value) {
|
||||
// If v is a named type and is addressable,
|
||||
// start with its address, so that if the type has pointer methods,
|
||||
// we find them.
|
||||
if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() {
|
||||
v = v.Addr()
|
||||
}
|
||||
for {
|
||||
// Load value from interface, but only if the result will be
|
||||
// usefully addressable.
|
||||
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||
e := v.Elem()
|
||||
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
|
||||
v = e
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Ptr {
|
||||
break
|
||||
}
|
||||
|
||||
if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() {
|
||||
break
|
||||
}
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
if v.Type().NumMethod() > 0 {
|
||||
if u, ok := v.Interface().(Unmarshaler); ok {
|
||||
return u, reflect.Value{}
|
||||
}
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
return nil, v
|
||||
}
|
||||
|
||||
// unquote converts a quoted EDN string literal s into an actual string t.
|
||||
// The rules are different than for Go, so cannot use strconv.Unquote.
|
||||
func unquote(s []byte) (t string, ok bool) {
|
||||
s, ok = unquoteBytes(s)
|
||||
t = string(s)
|
||||
return
|
||||
}
|
||||
|
||||
func unquoteBytes(s []byte) (t []byte, ok bool) {
|
||||
if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
|
||||
return
|
||||
}
|
||||
s = s[1 : len(s)-1]
|
||||
|
||||
// Check for unusual characters. If there are none,
|
||||
// then no unquoting is needed, so return a slice of the
|
||||
// original bytes.
|
||||
r := 0
|
||||
for r < len(s) {
|
||||
c := s[r]
|
||||
if c == '\\' || c == '"' {
|
||||
break
|
||||
}
|
||||
if c < utf8.RuneSelf {
|
||||
r++
|
||||
continue
|
||||
}
|
||||
rr, size := utf8.DecodeRune(s[r:])
|
||||
if rr == utf8.RuneError && size == 1 {
|
||||
break
|
||||
}
|
||||
r += size
|
||||
}
|
||||
if r == len(s) {
|
||||
return s, true
|
||||
}
|
||||
|
||||
b := make([]byte, len(s)+2*utf8.UTFMax)
|
||||
w := copy(b, s[0:r])
|
||||
for r < len(s) {
|
||||
// Out of room? Can only happen if s is full of
|
||||
// malformed UTF-8 and we're replacing each
|
||||
// byte with RuneError.
|
||||
if w >= len(b)-2*utf8.UTFMax {
|
||||
nb := make([]byte, (len(b)+utf8.UTFMax)*2)
|
||||
copy(nb, b[0:w])
|
||||
b = nb
|
||||
}
|
||||
switch c := s[r]; {
|
||||
case c == '\\':
|
||||
r++
|
||||
if r >= len(s) {
|
||||
return
|
||||
}
|
||||
switch s[r] {
|
||||
default:
|
||||
return
|
||||
case '"', '\\', '/', '\'':
|
||||
b[w] = s[r]
|
||||
r++
|
||||
w++
|
||||
case 'b':
|
||||
b[w] = '\b'
|
||||
r++
|
||||
w++
|
||||
case 'f':
|
||||
b[w] = '\f'
|
||||
r++
|
||||
w++
|
||||
case 'n':
|
||||
b[w] = '\n'
|
||||
r++
|
||||
w++
|
||||
case 'r':
|
||||
b[w] = '\r'
|
||||
r++
|
||||
w++
|
||||
case 't':
|
||||
b[w] = '\t'
|
||||
r++
|
||||
w++
|
||||
case 'u':
|
||||
r--
|
||||
rr := getu4(s[r:])
|
||||
if rr < 0 {
|
||||
return
|
||||
}
|
||||
r += 6
|
||||
w += utf8.EncodeRune(b[w:], rr)
|
||||
}
|
||||
|
||||
// Quote is invalid
|
||||
case c == '"':
|
||||
return
|
||||
|
||||
// ASCII
|
||||
case c < utf8.RuneSelf:
|
||||
b[w] = c
|
||||
r++
|
||||
w++
|
||||
|
||||
// Coerce to well-formed UTF-8.
|
||||
default:
|
||||
rr, size := utf8.DecodeRune(s[r:])
|
||||
r += size
|
||||
w += utf8.EncodeRune(b[w:], rr)
|
||||
}
|
||||
}
|
||||
return b[0:w], true
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
|
||||
kelvin = '\u212a'
|
||||
smallLongEss = '\u017f'
|
||||
)
|
||||
|
||||
// foldFunc returns one of four different case folding equivalence
|
||||
// functions, from most general (and slow) to fastest:
|
||||
//
|
||||
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
|
||||
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
|
||||
// 3) asciiEqualFold, no special, but includes non-letters (including _)
|
||||
// 4) simpleLetterEqualFold, no specials, no non-letters.
|
||||
//
|
||||
// The letters S and K are special because they map to 3 runes, not just 2:
|
||||
// * S maps to s and to U+017F 'ſ' Latin small letter long s
|
||||
// * k maps to K and to U+212A 'K' Kelvin sign
|
||||
// See https://play.golang.org/p/tTxjOc0OGo
|
||||
//
|
||||
// The returned function is specialized for matching against s and
|
||||
// should only be given s. It's not curried for performance reasons.
|
||||
func foldFunc(s []byte) func(s, t []byte) bool {
|
||||
nonLetter := false
|
||||
special := false // special letter
|
||||
for _, b := range s {
|
||||
if b >= utf8.RuneSelf {
|
||||
return bytes.EqualFold
|
||||
}
|
||||
upper := b & caseMask
|
||||
if upper < 'A' || upper > 'Z' {
|
||||
nonLetter = true
|
||||
} else if upper == 'K' || upper == 'S' {
|
||||
// See above for why these letters are special.
|
||||
special = true
|
||||
}
|
||||
}
|
||||
if special {
|
||||
return equalFoldRight
|
||||
}
|
||||
if nonLetter {
|
||||
return asciiEqualFold
|
||||
}
|
||||
return simpleLetterEqualFold
|
||||
}
|
||||
|
||||
// equalFoldRight is a specialization of bytes.EqualFold when s is
|
||||
// known to be all ASCII (including punctuation), but contains an 's',
|
||||
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
|
||||
// See comments on foldFunc.
|
||||
func equalFoldRight(s, t []byte) bool {
|
||||
for _, sb := range s {
|
||||
if len(t) == 0 {
|
||||
return false
|
||||
}
|
||||
tb := t[0]
|
||||
if tb < utf8.RuneSelf {
|
||||
if sb != tb {
|
||||
sbUpper := sb & caseMask
|
||||
if 'A' <= sbUpper && sbUpper <= 'Z' {
|
||||
if sbUpper != tb&caseMask {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
t = t[1:]
|
||||
continue
|
||||
}
|
||||
// sb is ASCII and t is not. t must be either kelvin
|
||||
// sign or long s; sb must be s, S, k, or K.
|
||||
tr, size := utf8.DecodeRune(t)
|
||||
switch sb {
|
||||
case 's', 'S':
|
||||
if tr != smallLongEss {
|
||||
return false
|
||||
}
|
||||
case 'k', 'K':
|
||||
if tr != kelvin {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
t = t[size:]
|
||||
|
||||
}
|
||||
if len(t) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// asciiEqualFold is a specialization of bytes.EqualFold for use when
|
||||
// s is all ASCII (but may contain non-letters) and contains no
|
||||
// special-folding letters.
|
||||
// See comments on foldFunc.
|
||||
func asciiEqualFold(s, t []byte) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i, sb := range s {
|
||||
tb := t[i]
|
||||
if sb == tb {
|
||||
continue
|
||||
}
|
||||
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
|
||||
if sb&caseMask != tb&caseMask {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// simpleLetterEqualFold is a specialization of bytes.EqualFold for
|
||||
// use when s is all ASCII letters (no underscores, etc) and also
|
||||
// doesn't contain 'k', 'K', 's', or 'S'.
|
||||
// See comments on foldFunc.
|
||||
func simpleLetterEqualFold(s, t []byte) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i, b := range s {
|
||||
if b&caseMask != t[i]&caseMask {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module olympos.io/encoding/edn
|
||||
|
||||
go 1.13
|
|
@ -0,0 +1,603 @@
|
|||
// Copyright 2015 Jean Niklas L'orange. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edn
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
u "unicode"
|
||||
)
|
||||
|
||||
type lexState int
|
||||
|
||||
const (
|
||||
lexCont = lexState(iota) // continue reading
|
||||
lexIgnore // values you can ignore, just whitespace and comments atm
|
||||
lexEnd // value ended with input given in
|
||||
lexEndPrev // value ended with previous input
|
||||
lexError // erroneous input
|
||||
)
|
||||
|
||||
type tokenType int
|
||||
|
||||
const ( // value types from lexer
|
||||
tokenSymbol = tokenType(iota)
|
||||
tokenKeyword
|
||||
tokenString
|
||||
tokenInt
|
||||
tokenFloat
|
||||
tokenTag
|
||||
tokenChar
|
||||
tokenListStart
|
||||
tokenListEnd
|
||||
tokenVectorStart
|
||||
tokenVectorEnd
|
||||
tokenMapStart
|
||||
tokenMapEnd
|
||||
tokenSetStart
|
||||
tokenDiscard
|
||||
|
||||
tokenError
|
||||
)
|
||||
|
||||
func (t tokenType) String() string {
|
||||
switch t {
|
||||
case tokenSymbol:
|
||||
return "symbol"
|
||||
case tokenKeyword:
|
||||
return "keyword"
|
||||
case tokenString:
|
||||
return "string"
|
||||
case tokenInt:
|
||||
return "integer"
|
||||
case tokenFloat:
|
||||
return "float"
|
||||
case tokenTag:
|
||||
return "tag"
|
||||
case tokenChar:
|
||||
return "character"
|
||||
case tokenListStart:
|
||||
return "list start"
|
||||
case tokenListEnd:
|
||||
return "list end"
|
||||
case tokenVectorStart:
|
||||
return "vector start"
|
||||
case tokenVectorEnd:
|
||||
return "vector end"
|
||||
case tokenMapStart:
|
||||
return "map start"
|
||||
case tokenMapEnd:
|
||||
return "map/set end"
|
||||
case tokenSetStart:
|
||||
return "set start"
|
||||
case tokenDiscard:
|
||||
return "discard token"
|
||||
case tokenError:
|
||||
return "error"
|
||||
default:
|
||||
return "[unknown]"
|
||||
}
|
||||
}
|
||||
|
||||
const tokenSetEnd = tokenMapEnd // sets ends the same way as maps do
|
||||
|
||||
// A SyntaxError is a description of an EDN syntax error.
|
||||
type SyntaxError struct {
|
||||
msg string // description of error
|
||||
Offset int64 // error occurred after reading Offset bytes
|
||||
}
|
||||
|
||||
func (e *SyntaxError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func okSymbolFirst(r rune) bool {
|
||||
switch r {
|
||||
case '.', '*', '+', '!', '-', '_', '?', '$', '%', '&', '=', '<', '>':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func okSymbol(r rune) bool {
|
||||
switch r {
|
||||
case '.', '*', '+', '!', '-', '_', '?', '$', '%', '&', '=', '<', '>', ':', '#', '\'':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isWhitespace(r rune) bool {
|
||||
return u.IsSpace(r) || r == ','
|
||||
}
|
||||
|
||||
type lexer struct {
|
||||
state func(rune) lexState
|
||||
err error
|
||||
position int64
|
||||
token tokenType
|
||||
|
||||
count int // counter is used in some functions within the lexer
|
||||
expecting []rune // expecting is used to avoid duplication when we expect e.g. \newline
|
||||
}
|
||||
|
||||
func (l *lexer) reset() {
|
||||
l.state = l.stateBegin
|
||||
l.token = tokenType(-1)
|
||||
l.err = nil
|
||||
}
|
||||
|
||||
func (l *lexer) eof() lexState {
|
||||
if l.err != nil {
|
||||
return lexError
|
||||
}
|
||||
lt := l.state(' ')
|
||||
if lt == lexCont {
|
||||
l.err = &SyntaxError{"unexpected end of EDN input", l.position}
|
||||
lt = lexError
|
||||
}
|
||||
if l.err != nil {
|
||||
return lexError
|
||||
}
|
||||
if lt == lexEndPrev {
|
||||
return lexEnd
|
||||
}
|
||||
return lt
|
||||
}
|
||||
|
||||
func (l *lexer) stateBegin(r rune) lexState {
|
||||
switch {
|
||||
case isWhitespace(r):
|
||||
return lexIgnore
|
||||
case r == '{':
|
||||
l.token = tokenMapStart
|
||||
return lexEnd
|
||||
case r == '}':
|
||||
l.token = tokenMapEnd
|
||||
return lexEnd
|
||||
case r == '[':
|
||||
l.token = tokenVectorStart
|
||||
return lexEnd
|
||||
case r == ']':
|
||||
l.token = tokenVectorEnd
|
||||
return lexEnd
|
||||
case r == '(':
|
||||
l.token = tokenListStart
|
||||
return lexEnd
|
||||
case r == ')':
|
||||
l.token = tokenListEnd
|
||||
return lexEnd
|
||||
case r == '#':
|
||||
l.state = l.statePound
|
||||
return lexCont
|
||||
case r == ':':
|
||||
l.state = l.stateKeyword
|
||||
return lexCont
|
||||
case r == '/': // ohh, the lovely slash edge case
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateEndLit
|
||||
return lexCont
|
||||
case r == '+':
|
||||
l.state = l.statePos
|
||||
return lexCont
|
||||
case r == '-':
|
||||
l.state = l.stateNeg
|
||||
return lexCont
|
||||
case r == '.':
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateDotPre
|
||||
return lexCont
|
||||
case r == '"':
|
||||
l.state = l.stateInString
|
||||
return lexCont
|
||||
case r == '\\':
|
||||
l.state = l.stateChar
|
||||
return lexCont
|
||||
case okSymbolFirst(r) || u.IsLetter(r):
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateSym
|
||||
return lexCont
|
||||
case '0' < r && r <= '9':
|
||||
l.state = l.state1
|
||||
return lexCont
|
||||
case r == '0':
|
||||
l.state = l.state0
|
||||
return lexCont
|
||||
case r == ';':
|
||||
l.state = l.stateComment
|
||||
return lexIgnore
|
||||
}
|
||||
return l.error(r, "- unexpected rune")
|
||||
}
|
||||
|
||||
func (l *lexer) stateComment(r rune) lexState {
|
||||
if r == '\n' {
|
||||
l.state = l.stateBegin
|
||||
}
|
||||
return lexIgnore
|
||||
}
|
||||
|
||||
func (l *lexer) stateEndLit(r rune) lexState {
|
||||
if isWhitespace(r) || r == '"' || r == '{' || r == '[' || r == '(' || r == ')' || r == ']' || r == '}' || r == '\\' || r == ';' {
|
||||
return lexEndPrev
|
||||
}
|
||||
return l.error(r, "- unexpected rune after legal "+l.token.String())
|
||||
}
|
||||
|
||||
func (l *lexer) stateKeyword(r rune) lexState {
|
||||
switch {
|
||||
case r == ':':
|
||||
l.state = l.stateError
|
||||
l.err = &SyntaxError{"EDN does not support namespace-qualified keywords", l.position}
|
||||
return lexError
|
||||
case r == '/':
|
||||
l.state = l.stateError
|
||||
l.err = &SyntaxError{"keywords cannot begin with /", l.position}
|
||||
return lexError
|
||||
case okSymbol(r) || u.IsLetter(r) || ('0' <= r && r <= '9'):
|
||||
l.token = tokenKeyword
|
||||
l.state = l.stateSym
|
||||
return lexCont
|
||||
}
|
||||
return l.error(r, "after keyword start")
|
||||
}
|
||||
|
||||
// examples: 'foo' 'bar'
|
||||
// we reuse this from the keyword states, so we don't set token at the end,
|
||||
// but before we call this
|
||||
func (l *lexer) stateSym(r rune) lexState {
|
||||
switch {
|
||||
case okSymbol(r) || u.IsLetter(r) || ('0' <= r && r <= '9'):
|
||||
l.state = l.stateSym
|
||||
return lexCont
|
||||
case r == '/':
|
||||
l.state = l.stateSlash
|
||||
return lexCont
|
||||
}
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// example: 'foo/'
|
||||
func (l *lexer) stateSlash(r rune) lexState {
|
||||
switch {
|
||||
case okSymbol(r) || u.IsLetter(r) || ('0' <= r && r <= '9'):
|
||||
l.state = l.statePostSlash
|
||||
return lexCont
|
||||
}
|
||||
return l.error(r, "directly after '/' in namespaced symbol")
|
||||
}
|
||||
|
||||
// example : 'foo/bar'
|
||||
func (l *lexer) statePostSlash(r rune) lexState {
|
||||
switch {
|
||||
case okSymbol(r) || u.IsLetter(r) || ('0' <= r && r <= '9'):
|
||||
l.state = l.statePostSlash
|
||||
return lexCont
|
||||
}
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// example: '-'
|
||||
func (l *lexer) stateNeg(r rune) lexState {
|
||||
switch {
|
||||
case r == '0':
|
||||
l.state = l.state0
|
||||
return lexCont
|
||||
case '1' <= r && r <= '9':
|
||||
l.state = l.state1
|
||||
return lexCont
|
||||
case okSymbol(r) || u.IsLetter(r):
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateSym
|
||||
return lexCont
|
||||
case r == '/':
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateSlash
|
||||
return lexCont
|
||||
}
|
||||
l.token = tokenSymbol
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// example: '+'
|
||||
func (l *lexer) statePos(r rune) lexState {
|
||||
switch {
|
||||
case r == '0':
|
||||
l.state = l.state0
|
||||
return lexCont
|
||||
case '1' <= r && r <= '9':
|
||||
l.state = l.state1
|
||||
return lexCont
|
||||
case okSymbol(r) || u.IsLetter(r):
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateSym
|
||||
return lexCont
|
||||
case r == '/':
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateSlash
|
||||
return lexCont
|
||||
}
|
||||
l.token = tokenSymbol
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// value is '0'
|
||||
func (l *lexer) state0(r rune) lexState {
|
||||
switch {
|
||||
case r == '.':
|
||||
l.state = l.stateDot
|
||||
return lexCont
|
||||
case r == 'e' || r == 'E':
|
||||
l.state = l.stateE
|
||||
return lexCont
|
||||
case r == 'M': // bigdecimal
|
||||
l.token = tokenFloat
|
||||
l.state = l.stateEndLit
|
||||
return lexCont // must be ws or delimiter afterwards
|
||||
case r == 'N': // bigint
|
||||
l.token = tokenInt
|
||||
l.state = l.stateEndLit
|
||||
return lexCont // must be ws or delimiter afterwards
|
||||
}
|
||||
l.token = tokenInt
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// anything but a result starting with 0. example '10', '34'
|
||||
func (l *lexer) state1(r rune) lexState {
|
||||
if '0' <= r && r <= '9' {
|
||||
return lexCont
|
||||
}
|
||||
return l.state0(r)
|
||||
}
|
||||
|
||||
// example: '.', can only receive non-numerics here
|
||||
func (l *lexer) stateDotPre(r rune) lexState {
|
||||
switch {
|
||||
case okSymbol(r) || u.IsLetter(r):
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateSym
|
||||
return lexCont
|
||||
case r == '/':
|
||||
l.token = tokenSymbol
|
||||
l.state = l.stateSlash
|
||||
return lexCont
|
||||
}
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// after reading numeric values plus '.', example: '12.'
|
||||
func (l *lexer) stateDot(r rune) lexState {
|
||||
if '0' <= r && r <= '9' {
|
||||
l.state = l.stateDot0
|
||||
return lexCont
|
||||
}
|
||||
// TODO (?): The spec says that there must be numbers after the dot, yet
|
||||
// (clojure.edn/read-string "1.e1") returns 10.0
|
||||
return l.error(r, "after decimal point in numeric literal")
|
||||
}
|
||||
|
||||
// after reading numeric values plus '.', example: '12.34'
|
||||
func (l *lexer) stateDot0(r rune) lexState {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
return lexCont
|
||||
case r == 'e' || r == 'E':
|
||||
l.state = l.stateE
|
||||
return lexCont
|
||||
case r == 'M':
|
||||
l.token = tokenFloat
|
||||
l.state = l.stateEndLit
|
||||
return lexCont
|
||||
}
|
||||
l.token = tokenFloat
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// stateE is the state after reading the mantissa and e in a number,
|
||||
// such as after reading `314e` or `0.314e`.
|
||||
func (l *lexer) stateE(r rune) lexState {
|
||||
if r == '+' || r == '-' {
|
||||
l.state = l.stateESign
|
||||
return lexCont
|
||||
}
|
||||
return l.stateESign(r)
|
||||
}
|
||||
|
||||
// stateESign is the state after reading the mantissa, e, and sign in a number,
|
||||
// such as after reading `314e-` or `0.314e+`.
|
||||
func (l *lexer) stateESign(r rune) lexState {
|
||||
if '0' <= r && r <= '9' {
|
||||
l.state = l.stateE0
|
||||
return lexCont
|
||||
}
|
||||
return l.error(r, "in exponent of numeric literal")
|
||||
}
|
||||
|
||||
// stateE0 is the state after reading the mantissa, e, optional sign,
|
||||
// and at least one digit of the exponent in a number,
|
||||
// such as after reading `314e-2` or `0.314e+1` or `3.14e0`.
|
||||
func (l *lexer) stateE0(r rune) lexState {
|
||||
if '0' <= r && r <= '9' {
|
||||
return lexCont
|
||||
}
|
||||
if r == 'M' {
|
||||
l.token = tokenFloat
|
||||
l.state = l.stateEndLit
|
||||
return lexCont
|
||||
}
|
||||
l.token = tokenFloat
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
var (
|
||||
newlineRunes = []rune("newline")
|
||||
returnRunes = []rune("return")
|
||||
spaceRunes = []rune("space")
|
||||
tabRunes = []rune("tab")
|
||||
formfeedRunes = []rune("formfeed")
|
||||
)
|
||||
|
||||
// stateChar after a backslash ('\')
|
||||
func (l *lexer) stateChar(r rune) lexState {
|
||||
switch {
|
||||
// oh my, I'm so happy that none of these share the same prefix.
|
||||
case r == 'n':
|
||||
l.count = 1
|
||||
l.expecting = newlineRunes
|
||||
l.state = l.stateSpecialChar
|
||||
return lexCont
|
||||
case r == 'r':
|
||||
l.count = 1
|
||||
l.expecting = returnRunes
|
||||
l.state = l.stateSpecialChar
|
||||
return lexCont
|
||||
case r == 's':
|
||||
l.count = 1
|
||||
l.expecting = spaceRunes
|
||||
l.state = l.stateSpecialChar
|
||||
return lexCont
|
||||
case r == 't':
|
||||
l.count = 1
|
||||
l.expecting = tabRunes
|
||||
l.state = l.stateSpecialChar
|
||||
return lexCont
|
||||
case r == 'f':
|
||||
l.count = 1
|
||||
l.expecting = formfeedRunes
|
||||
l.state = l.stateSpecialChar
|
||||
return lexCont
|
||||
case r == 'u':
|
||||
l.count = 0
|
||||
l.state = l.stateUnicodeChar
|
||||
return lexCont
|
||||
case isWhitespace(r):
|
||||
l.state = l.stateError
|
||||
l.err = &SyntaxError{"backslash cannot be followed by whitespace", l.position}
|
||||
return lexError
|
||||
}
|
||||
// default is single name character
|
||||
l.token = tokenChar
|
||||
l.state = l.stateEndLit
|
||||
return lexCont
|
||||
}
|
||||
|
||||
func (l *lexer) stateSpecialChar(r rune) lexState {
|
||||
if r == l.expecting[l.count] {
|
||||
l.count++
|
||||
if l.count == len(l.expecting) {
|
||||
l.token = tokenChar
|
||||
l.state = l.stateEndLit
|
||||
return lexCont
|
||||
}
|
||||
return lexCont
|
||||
}
|
||||
if l.count != 1 {
|
||||
return l.error(r, "after start of special character")
|
||||
}
|
||||
// it is likely just a normal character, like 'n' or 't'
|
||||
l.token = tokenChar
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
func (l *lexer) stateUnicodeChar(r rune) lexState {
|
||||
if '0' <= r && r <= '9' || 'a' <= r && r <= 'f' || 'A' <= r && r <= 'F' {
|
||||
l.count++
|
||||
if l.count == 4 {
|
||||
l.token = tokenChar
|
||||
l.state = l.stateEndLit
|
||||
}
|
||||
return lexCont
|
||||
}
|
||||
if l.count != 0 {
|
||||
return l.error(r, "after start of unicode character")
|
||||
}
|
||||
// likely just '\u'
|
||||
l.token = tokenChar
|
||||
return l.stateEndLit(r)
|
||||
}
|
||||
|
||||
// stateInString is the state after reading `"`.
|
||||
func (l *lexer) stateInString(r rune) lexState {
|
||||
if r == '"' {
|
||||
l.token = tokenString
|
||||
return lexEnd
|
||||
}
|
||||
if r == '\\' {
|
||||
l.state = l.stateInStringEsc
|
||||
return lexCont
|
||||
}
|
||||
return lexCont
|
||||
}
|
||||
|
||||
// stateInStringEsc is the state after reading `"\` during a quoted string.
|
||||
func (l *lexer) stateInStringEsc(r rune) lexState {
|
||||
switch r {
|
||||
case 'b', 'f', 'n', 'r', 't', '\\', '/', '"':
|
||||
l.state = l.stateInString
|
||||
return lexCont
|
||||
case 'u':
|
||||
l.state = l.stateInStringEscU
|
||||
l.count = 0
|
||||
return lexCont
|
||||
}
|
||||
return l.error(r, "in string escape code")
|
||||
}
|
||||
|
||||
// stateInStringEscU is the state after reading `"\u` and l.count elements in a
|
||||
// quoted string.
|
||||
func (l *lexer) stateInStringEscU(r rune) lexState {
|
||||
if '0' <= r && r <= '9' || 'a' <= r && r <= 'f' || 'A' <= r && r <= 'F' {
|
||||
l.count++
|
||||
if l.count == 4 {
|
||||
l.state = l.stateInString
|
||||
}
|
||||
return lexCont
|
||||
}
|
||||
// numbers
|
||||
return l.error(r, "in \\u hexadecimal character escape")
|
||||
}
|
||||
|
||||
// after reading the character '#'
|
||||
func (l *lexer) statePound(r rune) lexState {
|
||||
switch {
|
||||
case r == '_':
|
||||
l.token = tokenDiscard
|
||||
return lexEnd
|
||||
case r == '{':
|
||||
l.token = tokenSetStart
|
||||
return lexEnd
|
||||
case u.IsLetter(r):
|
||||
l.token = tokenTag
|
||||
l.state = l.stateSym
|
||||
return lexCont
|
||||
}
|
||||
return l.error(r, `after token starting with "#"`)
|
||||
}
|
||||
|
||||
func (l *lexer) stateError(r rune) lexState {
|
||||
return lexError
|
||||
}
|
||||
|
||||
// error records an error and switches to the error state.
|
||||
func (l *lexer) error(r rune, context string) lexState {
|
||||
l.state = l.stateError
|
||||
l.err = &SyntaxError{"invalid character " + quoteRune(r) + " " + context, l.position}
|
||||
return lexError
|
||||
}
|
||||
|
||||
// quoteRune formats r as a quoted rune literal
|
||||
func quoteRune(r rune) string {
|
||||
// special cases - different from quoted strings
|
||||
if r == '\'' {
|
||||
return `'\''`
|
||||
}
|
||||
if r == '"' {
|
||||
return `'"'`
|
||||
}
|
||||
|
||||
// use quoted string with different quotation marks
|
||||
s := strconv.Quote(string(r))
|
||||
return "'" + s[1:len(s)-1] + "'"
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
// Copyright 2015 Jean Niklas L'orange. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
// we can't call it spaceBytes not to conflict with decode.go's spaceBytes.
|
||||
spaceOutputBytes = []byte(" ")
|
||||
commaOutputBytes = []byte(",")
|
||||
)
|
||||
|
||||
func newline(dst io.Writer, prefix, indent string, depth int) {
|
||||
dst.Write([]byte{'\n'})
|
||||
dst.Write([]byte(prefix))
|
||||
for i := 0; i < depth; i++ {
|
||||
dst.Write([]byte(indent))
|
||||
}
|
||||
}
|
||||
|
||||
// Indent writes to dst an indented form of the EDN-encoded src. Each EDN
|
||||
// collection begins on a new, indented line beginning with prefix followed by
|
||||
// one or more copies of indent according to the indentation nesting. The data
|
||||
// written to dst does not begin with the prefix nor any indentation, and has
|
||||
// no trailing newline, to make it easier to embed inside other formatted EDN
|
||||
// data.
|
||||
//
|
||||
// Indent filters away whitespace, including comments and discards.
|
||||
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
origLen := dst.Len()
|
||||
err := IndentStream(dst, bytes.NewBuffer(src), prefix, indent)
|
||||
if err != nil {
|
||||
dst.Truncate(origLen)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// IndentStream is an implementation of PPrint for generic readers and writers
|
||||
func IndentStream(dst io.Writer, src io.Reader, prefix, indent string) error {
|
||||
var lex lexer
|
||||
lex.reset()
|
||||
tokStack := newTokenStack()
|
||||
curType := tokenError
|
||||
curSize := 0
|
||||
d := NewDecoder(src)
|
||||
depth := 0
|
||||
for {
|
||||
bs, tt, err := d.nextToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tokStack.push(tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prevType := curType
|
||||
prevSize := curSize
|
||||
if len(tokStack.toks) > 0 {
|
||||
curType = tokStack.peek()
|
||||
curSize = tokStack.peekCount()
|
||||
}
|
||||
switch tt {
|
||||
case tokenMapStart, tokenVectorStart, tokenListStart, tokenSetStart:
|
||||
if prevType == tokenMapStart {
|
||||
dst.Write([]byte{' '})
|
||||
} else if depth > 0 {
|
||||
newline(dst, prefix, indent, depth)
|
||||
}
|
||||
dst.Write(bs)
|
||||
depth++
|
||||
case tokenVectorEnd, tokenListEnd, tokenMapEnd: // tokenSetEnd == tokenMapEnd
|
||||
depth--
|
||||
if prevSize > 0 { // suppress indent for empty collections
|
||||
newline(dst, prefix, indent, depth)
|
||||
}
|
||||
// all of these are of length 1 in bytes, so utilise this for perf
|
||||
dst.Write(bs)
|
||||
case tokenTag:
|
||||
// need to know what the previous type was.
|
||||
switch prevType {
|
||||
case tokenMapStart:
|
||||
if prevSize%2 == 0 { // If previous size modulo 2 is equal to 0, we're a key
|
||||
if prevSize > 0 {
|
||||
dst.Write(commaOutputBytes)
|
||||
}
|
||||
newline(dst, prefix, indent, depth)
|
||||
} else { // We're a value, add a space after the key
|
||||
dst.Write(spaceOutputBytes)
|
||||
}
|
||||
dst.Write(bs)
|
||||
dst.Write(spaceOutputBytes)
|
||||
case tokenSetStart, tokenVectorStart, tokenListStart:
|
||||
newline(dst, prefix, indent, depth)
|
||||
dst.Write(bs)
|
||||
dst.Write(spaceOutputBytes)
|
||||
default: // tokenError or nested tag
|
||||
dst.Write(bs)
|
||||
dst.Write(spaceOutputBytes)
|
||||
}
|
||||
default:
|
||||
switch prevType {
|
||||
case tokenMapStart:
|
||||
if prevSize%2 == 0 { // If previous size modulo 2 is equal to 0, we're a key
|
||||
if prevSize > 0 {
|
||||
dst.Write(commaOutputBytes)
|
||||
}
|
||||
newline(dst, prefix, indent, depth)
|
||||
} else { // We're a value, add a space after the key
|
||||
dst.Write(spaceOutputBytes)
|
||||
}
|
||||
dst.Write(bs)
|
||||
case tokenSetStart, tokenVectorStart, tokenListStart:
|
||||
newline(dst, prefix, indent, depth)
|
||||
dst.Write(bs)
|
||||
default: // toplevel or nested tag. This should collapse the whole tag tower
|
||||
dst.Write(bs)
|
||||
}
|
||||
}
|
||||
if tokStack.done() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PPrintOpts is a configuration map for PPrint. The values in this struct has
|
||||
// no effect as of now.
|
||||
type PPrintOpts struct {
|
||||
RightMargin int
|
||||
MiserWidth int
|
||||
}
|
||||
|
||||
func pprintIndent(dst io.Writer, shift int) {
|
||||
spaces := make([]byte, shift+1)
|
||||
|
||||
spaces[0] = '\n'
|
||||
|
||||
// TODO: This may be slower than caching the size as a byte slice
|
||||
for i := 1; i <= shift; i++ {
|
||||
spaces[i] = ' '
|
||||
}
|
||||
|
||||
dst.Write(spaces)
|
||||
}
|
||||
|
||||
// PPrint writes to dst an indented form of the EDN-encoded src. This
|
||||
// implementation attempts to write idiomatic/readable EDN values, in a fashion
|
||||
// close to (but not quite equal to) clojure.pprint/pprint.
|
||||
//
|
||||
// PPrint filters away whitespace, including comments and discards.
|
||||
func PPrint(dst *bytes.Buffer, src []byte, opt *PPrintOpts) error {
|
||||
origLen := dst.Len()
|
||||
err := PPrintStream(dst, bytes.NewBuffer(src), opt)
|
||||
if err != nil {
|
||||
dst.Truncate(origLen)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// PPrintStream is an implementation of PPrint for generic readers and writers
|
||||
func PPrintStream(dst io.Writer, src io.Reader, opt *PPrintOpts) error {
|
||||
var lex lexer
|
||||
var col, prevCollStart, curSize int
|
||||
var prevColl bool
|
||||
|
||||
lex.reset()
|
||||
tokStack := newTokenStack()
|
||||
|
||||
shift := make([]int, 1, 8) // pre-allocate some space
|
||||
curType := tokenError
|
||||
d := NewDecoder(src)
|
||||
|
||||
for {
|
||||
bs, tt, err := d.nextToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tokStack.push(tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prevType := curType
|
||||
prevSize := curSize
|
||||
if len(tokStack.toks) > 0 {
|
||||
curType = tokStack.peek()
|
||||
curSize = tokStack.peekCount()
|
||||
}
|
||||
// Indentation
|
||||
switch tt {
|
||||
case tokenVectorEnd, tokenListEnd, tokenMapEnd:
|
||||
default:
|
||||
switch prevType {
|
||||
case tokenMapStart:
|
||||
if prevSize%2 == 0 && prevSize > 0 {
|
||||
dst.Write(commaOutputBytes)
|
||||
pprintIndent(dst, shift[len(shift)-1])
|
||||
col = shift[len(shift)-1]
|
||||
} else if prevSize%2 == 1 { // We're a value, add a space after the key
|
||||
dst.Write(spaceOutputBytes)
|
||||
col++
|
||||
}
|
||||
case tokenSetStart, tokenVectorStart, tokenListStart:
|
||||
if prevColl {
|
||||
// begin on new line where prevColl started
|
||||
// This will look so strange for heterogenous maps.
|
||||
pprintIndent(dst, prevCollStart)
|
||||
col = prevCollStart
|
||||
} else if prevSize > 0 {
|
||||
dst.Write(spaceOutputBytes)
|
||||
col++
|
||||
}
|
||||
}
|
||||
}
|
||||
switch tt {
|
||||
case tokenMapStart, tokenVectorStart, tokenListStart, tokenSetStart:
|
||||
dst.Write(bs)
|
||||
col += len(bs) // either 2 or 1
|
||||
shift = append(shift, col) // we only use maps for now, but we'll utilise this more thoroughly later on
|
||||
case tokenVectorEnd, tokenListEnd, tokenMapEnd: // tokenSetEnd == tokenMapEnd
|
||||
dst.Write(bs) // all of these are of length 1 in bytes, so this is ok
|
||||
prevCollStart = shift[len(shift)-1] - 1
|
||||
shift = shift[:len(shift)-1]
|
||||
case tokenTag:
|
||||
bslen := utf8.RuneCount(bs)
|
||||
dst.Write(bs)
|
||||
dst.Write(spaceOutputBytes)
|
||||
col += bslen + 1
|
||||
default:
|
||||
bslen := utf8.RuneCount(bs)
|
||||
dst.Write(bs)
|
||||
col += bslen
|
||||
}
|
||||
prevColl = (tt == tokenMapEnd || tt == tokenVectorEnd || tt == tokenListEnd)
|
||||
if tokStack.done() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edn
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tagOptions is the string following a comma in a struct field's "json"
|
||||
// tag, or the empty string. It does not include the leading comma.
|
||||
type tagOptions string
|
||||
|
||||
// parseTag splits a struct field's json tag into its name and
|
||||
// comma-separated options.
|
||||
func parseTag(tag string) (string, tagOptions) {
|
||||
if idx := strings.Index(tag, ","); idx != -1 {
|
||||
return tag[:idx], tagOptions(tag[idx+1:])
|
||||
}
|
||||
return tag, tagOptions("")
|
||||
}
|
||||
|
||||
// Contains reports whether a comma-separated list of options
|
||||
// contains a particular substr flag. substr must be surrounded by a
|
||||
// string boundary or commas.
|
||||
func (o tagOptions) Contains(optionName string) bool {
|
||||
if len(o) == 0 {
|
||||
return false
|
||||
}
|
||||
s := string(o)
|
||||
for s != "" {
|
||||
var next string
|
||||
i := strings.Index(s, ",")
|
||||
if i >= 0 {
|
||||
s, next = s[:i], s[i+1:]
|
||||
}
|
||||
if s == optionName {
|
||||
return true
|
||||
}
|
||||
s = next
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2015-2017 Jean Niklas L'orange. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edn
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RawMessage is a raw encoded, but valid, EDN value. It implements Marshaler
|
||||
// and Unmarshaler and can be used to delay EDN decoding or precompute an EDN
|
||||
// encoding.
|
||||
type RawMessage []byte
|
||||
|
||||
// MarshalEDN returns m as the EDN encoding of m.
|
||||
func (m RawMessage) MarshalEDN() ([]byte, error) {
|
||||
if m == nil {
|
||||
return []byte("nil"), nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// UnmarshalEDN sets *m to a copy of data.
|
||||
func (m *RawMessage) UnmarshalEDN(data []byte) error {
|
||||
if m == nil {
|
||||
return errors.New("edn.RawMessage: UnmarshalEDN on nil pointer")
|
||||
}
|
||||
*m = append((*m)[0:0], data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// A Keyword is an EDN keyword without : prepended in front.
|
||||
type Keyword string
|
||||
|
||||
func (k Keyword) String() string {
|
||||
return fmt.Sprintf(":%s", string(k))
|
||||
}
|
||||
|
||||
func (k Keyword) MarshalEDN() ([]byte, error) {
|
||||
return []byte(k.String()), nil
|
||||
}
|
||||
|
||||
// A Symbol is an EDN symbol.
|
||||
type Symbol string
|
||||
|
||||
func (s Symbol) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s Symbol) MarshalEDN() ([]byte, error) {
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
// A Tag is a tagged value. The Tagname represents the name of the tag, and the
|
||||
// Value is the value of the element.
|
||||
type Tag struct {
|
||||
Tagname string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func (t Tag) String() string {
|
||||
return fmt.Sprintf("#%s %v", t.Tagname, t.Value)
|
||||
}
|
||||
|
||||
func (t Tag) MarshalEDN() ([]byte, error) {
|
||||
str := []byte(fmt.Sprintf(`#%s `, t.Tagname))
|
||||
b, err := Marshal(t.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(str, b...), nil
|
||||
}
|
||||
|
||||
func (t *Tag) UnmarshalEDN(bs []byte) error {
|
||||
// read actual tag, using the lexer.
|
||||
var lex lexer
|
||||
lex.reset()
|
||||
buf := bufio.NewReader(bytes.NewBuffer(bs))
|
||||
start := 0
|
||||
endTag := 0
|
||||
tag:
|
||||
for {
|
||||
r, rlen, err := buf.ReadRune()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ls := lex.state(r)
|
||||
switch ls {
|
||||
case lexIgnore:
|
||||
start += rlen
|
||||
endTag += rlen
|
||||
case lexError:
|
||||
return lex.err
|
||||
case lexEndPrev:
|
||||
break tag
|
||||
case lexEnd: // unexpected, assuming tag which is not ending with lexEnd
|
||||
return errUnexpected
|
||||
case lexCont:
|
||||
endTag += rlen
|
||||
}
|
||||
}
|
||||
t.Tagname = string(bs[start+1 : endTag])
|
||||
return Unmarshal(bs[endTag:], &t.Value)
|
||||
}
|
||||
|
||||
// A Rune type is a wrapper for a rune. It can be used to encode runes as
|
||||
// characters instead of int32 values.
|
||||
type Rune rune
|
||||
|
||||
func (r Rune) MarshalEDN() ([]byte, error) {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 10))
|
||||
encodeRune(buf, rune(r))
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func encodeRune(buf *bytes.Buffer, r rune) {
|
||||
const hex = "0123456789abcdef"
|
||||
if !isWhitespace(r) {
|
||||
buf.WriteByte('\\')
|
||||
buf.WriteRune(r)
|
||||
} else {
|
||||
switch r {
|
||||
case '\b':
|
||||
buf.WriteString(`\backspace`)
|
||||
case '\f':
|
||||
buf.WriteString(`\formfeed`)
|
||||
case '\n':
|
||||
buf.WriteString(`\newline`)
|
||||
case '\r':
|
||||
buf.WriteString(`\return`)
|
||||
case '\t':
|
||||
buf.WriteString(`\tab`)
|
||||
case ' ':
|
||||
buf.WriteString(`\space`)
|
||||
default:
|
||||
buf.WriteByte('\\')
|
||||
buf.WriteByte('u')
|
||||
buf.WriteByte(hex[r>>12&0xF])
|
||||
buf.WriteByte(hex[r>>8&0xF])
|
||||
buf.WriteByte(hex[r>>4&0xF])
|
||||
buf.WriteByte(hex[r&0xF])
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue