add modules

This commit is contained in:
Solovyov1796 2024-04-25 15:31:20 +08:00
parent d9f60f1dde
commit 30084913dd
216 changed files with 56282 additions and 81 deletions

View File

@ -32,6 +32,9 @@ import (
"github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply" "github.com/cosmos/cosmos-sdk/x/supply"
"github.com/0glabs/0g-chain/x/bep3"
"github.com/0glabs/0g-chain/x/committee"
"github.com/0glabs/0g-chain/x/pricefeed"
validatorvesting "github.com/0glabs/0g-chain/x/validator-vesting" validatorvesting "github.com/0glabs/0g-chain/x/validator-vesting"
) )
@ -179,6 +182,7 @@ func TestAppImportExport(t *testing.T) {
{app.keys[kavadist.StoreKey], newApp.keys[kavadist.StoreKey], [][]byte{}}, {app.keys[kavadist.StoreKey], newApp.keys[kavadist.StoreKey], [][]byte{}},
{app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}}, {app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}},
{app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}}, {app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}},
{app.keys[committee.StoreKey], newApp.keys[committee.StoreKey], [][]byte{}},
{app.keys[council.StoreKey], newApp.keys[council.StoreKey], [][]byte{}}, {app.keys[council.StoreKey], newApp.keys[council.StoreKey], [][]byte{}},
{app.keys[swap.StoreKey], newApp.keys[swap.StoreKey], [][]byte{}}, {app.keys[swap.StoreKey], newApp.keys[swap.StoreKey], [][]byte{}},
} }

View File

@ -22,8 +22,8 @@ import (
"github.com/0glabs/0g-chain/app" "github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/chaincfg" "github.com/0glabs/0g-chain/chaincfg"
// bep3types "github.com/0glabs/0g-chain/x/bep3/types" bep3types "github.com/0glabs/0g-chain/x/bep3/types"
// pricefeedtypes "github.com/0glabs/0g-chain/x/pricefeed/types" pricefeedtypes "github.com/0glabs/0g-chain/x/pricefeed/types"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -35,10 +35,10 @@ func TestAppAnteHandler_AuthorizedMempool(t *testing.T) {
testPrivKeys, testAddresses := app.GeneratePrivKeyAddressPairs(10) testPrivKeys, testAddresses := app.GeneratePrivKeyAddressPairs(10)
unauthed := testAddresses[0:2] unauthed := testAddresses[0:2]
unauthedKeys := testPrivKeys[0:2] unauthedKeys := testPrivKeys[0:2]
// deputy := testAddresses[2] deputy := testAddresses[2]
// deputyKey := testPrivKeys[2] deputyKey := testPrivKeys[2]
// oracles := testAddresses[3:6] oracles := testAddresses[3:6]
// oraclesKeys := testPrivKeys[3:6] oraclesKeys := testPrivKeys[3:6]
manual := testAddresses[6:] manual := testAddresses[6:]
manualKeys := testPrivKeys[6:] manualKeys := testPrivKeys[6:]
@ -69,8 +69,8 @@ func TestAppAnteHandler_AuthorizedMempool(t *testing.T) {
sdk.NewCoins(sdk.NewInt64Coin(chaincfg.DisplayDenom, 1e9)), sdk.NewCoins(sdk.NewInt64Coin(chaincfg.DisplayDenom, 1e9)),
testAddresses, testAddresses,
), ),
// newBep3GenStateMulti(tApp.AppCodec(), deputy), newBep3GenStateMulti(tApp.AppCodec(), deputy),
// newPricefeedGenStateMulti(tApp.AppCodec(), oracles), newPricefeedGenStateMulti(tApp.AppCodec(), oracles),
) )
testcases := []struct { testcases := []struct {
@ -85,18 +85,18 @@ func TestAppAnteHandler_AuthorizedMempool(t *testing.T) {
privKey: unauthedKeys[1], privKey: unauthedKeys[1],
expectPass: false, expectPass: false,
}, },
// { {
// name: "oracle", name: "oracle",
// address: oracles[1], address: oracles[1],
// privKey: oraclesKeys[1], privKey: oraclesKeys[1],
// expectPass: true, expectPass: true,
// }, },
// { {
// name: "deputy", name: "deputy",
// address: deputy, address: deputy,
// privKey: deputyKey, privKey: deputyKey,
// expectPass: true, expectPass: true,
// }, },
{ {
name: "manual", name: "manual",
address: manual[1], address: manual[1],
@ -144,53 +144,53 @@ func TestAppAnteHandler_AuthorizedMempool(t *testing.T) {
} }
} }
// func newPricefeedGenStateMulti(cdc codec.JSONCodec, oracles []sdk.AccAddress) app.GenesisState { func newPricefeedGenStateMulti(cdc codec.JSONCodec, oracles []sdk.AccAddress) app.GenesisState {
// pfGenesis := pricefeedtypes.GenesisState{ pfGenesis := pricefeedtypes.GenesisState{
// Params: pricefeedtypes.Params{ Params: pricefeedtypes.Params{
// Markets: []pricefeedtypes.Market{ Markets: []pricefeedtypes.Market{
// {MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: oracles, Active: true}, {MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: oracles, Active: true},
// }, },
// }, },
// } }
// return app.GenesisState{pricefeedtypes.ModuleName: cdc.MustMarshalJSON(&pfGenesis)} return app.GenesisState{pricefeedtypes.ModuleName: cdc.MustMarshalJSON(&pfGenesis)}
// } }
// func newBep3GenStateMulti(cdc codec.JSONCodec, deputyAddress sdk.AccAddress) app.GenesisState { func newBep3GenStateMulti(cdc codec.JSONCodec, deputyAddress sdk.AccAddress) app.GenesisState {
// bep3Genesis := bep3types.GenesisState{ bep3Genesis := bep3types.GenesisState{
// Params: bep3types.Params{ Params: bep3types.Params{
// AssetParams: bep3types.AssetParams{ AssetParams: bep3types.AssetParams{
// bep3types.AssetParam{ bep3types.AssetParam{
// Denom: "bnb", Denom: "bnb",
// CoinID: 714, CoinID: 714,
// SupplyLimit: bep3types.SupplyLimit{ SupplyLimit: bep3types.SupplyLimit{
// Limit: sdkmath.NewInt(350000000000000), Limit: sdkmath.NewInt(350000000000000),
// TimeLimited: false, TimeLimited: false,
// TimeBasedLimit: sdk.ZeroInt(), TimeBasedLimit: sdk.ZeroInt(),
// TimePeriod: time.Hour, TimePeriod: time.Hour,
// }, },
// Active: true, Active: true,
// DeputyAddress: deputyAddress, DeputyAddress: deputyAddress,
// FixedFee: sdkmath.NewInt(1000), FixedFee: sdkmath.NewInt(1000),
// MinSwapAmount: sdk.OneInt(), MinSwapAmount: sdk.OneInt(),
// MaxSwapAmount: sdkmath.NewInt(1000000000000), MaxSwapAmount: sdkmath.NewInt(1000000000000),
// MinBlockLock: bep3types.DefaultMinBlockLock, MinBlockLock: bep3types.DefaultMinBlockLock,
// MaxBlockLock: bep3types.DefaultMaxBlockLock, MaxBlockLock: bep3types.DefaultMaxBlockLock,
// }, },
// }, },
// }, },
// Supplies: bep3types.AssetSupplies{ Supplies: bep3types.AssetSupplies{
// bep3types.NewAssetSupply( bep3types.NewAssetSupply(
// sdk.NewCoin("bnb", sdk.ZeroInt()), sdk.NewCoin("bnb", sdk.ZeroInt()),
// sdk.NewCoin("bnb", sdk.ZeroInt()), sdk.NewCoin("bnb", sdk.ZeroInt()),
// sdk.NewCoin("bnb", sdk.ZeroInt()), sdk.NewCoin("bnb", sdk.ZeroInt()),
// sdk.NewCoin("bnb", sdk.ZeroInt()), sdk.NewCoin("bnb", sdk.ZeroInt()),
// time.Duration(0), time.Duration(0),
// ), ),
// }, },
// PreviousBlockTime: bep3types.DefaultPreviousBlockTime, PreviousBlockTime: bep3types.DefaultPreviousBlockTime,
// } }
// return app.GenesisState{bep3types.ModuleName: cdc.MustMarshalJSON(&bep3Genesis)} return app.GenesisState{bep3types.ModuleName: cdc.MustMarshalJSON(&bep3Genesis)}
// } }
func TestAppAnteHandler_RejectMsgsInAuthz(t *testing.T) { func TestAppAnteHandler_RejectMsgsInAuthz(t *testing.T) {
testPrivKeys, testAddresses := app.GeneratePrivKeyAddressPairs(10) testPrivKeys, testAddresses := app.GeneratePrivKeyAddressPairs(10)

View File

@ -103,17 +103,29 @@ import (
"github.com/0glabs/0g-chain/app/ante" "github.com/0glabs/0g-chain/app/ante"
chainparams "github.com/0glabs/0g-chain/app/params" chainparams "github.com/0glabs/0g-chain/app/params"
"github.com/0glabs/0g-chain/chaincfg" "github.com/0glabs/0g-chain/chaincfg"
"github.com/0glabs/0g-chain/x/bep3"
bep3keeper "github.com/0glabs/0g-chain/x/bep3/keeper"
bep3types "github.com/0glabs/0g-chain/x/bep3/types"
evmutil "github.com/0glabs/0g-chain/x/evmutil" evmutil "github.com/0glabs/0g-chain/x/evmutil"
evmutilkeeper "github.com/0glabs/0g-chain/x/evmutil/keeper" evmutilkeeper "github.com/0glabs/0g-chain/x/evmutil/keeper"
evmutiltypes "github.com/0glabs/0g-chain/x/evmutil/types" evmutiltypes "github.com/0glabs/0g-chain/x/evmutil/types"
"github.com/0glabs/0g-chain/x/committee"
committeeclient "github.com/0glabs/0g-chain/x/committee/client"
committeekeeper "github.com/0glabs/0g-chain/x/committee/keeper"
committeetypes "github.com/0glabs/0g-chain/x/committee/types"
council "github.com/0glabs/0g-chain/x/council/v1" council "github.com/0glabs/0g-chain/x/council/v1"
councilkeeper "github.com/0glabs/0g-chain/x/council/v1/keeper" councilkeeper "github.com/0glabs/0g-chain/x/council/v1/keeper"
counciltypes "github.com/0glabs/0g-chain/x/council/v1/types" counciltypes "github.com/0glabs/0g-chain/x/council/v1/types"
das "github.com/0glabs/0g-chain/x/das/v1" das "github.com/0glabs/0g-chain/x/das/v1"
daskeeper "github.com/0glabs/0g-chain/x/das/v1/keeper" daskeeper "github.com/0glabs/0g-chain/x/das/v1/keeper"
dastypes "github.com/0glabs/0g-chain/x/das/v1/types" dastypes "github.com/0glabs/0g-chain/x/das/v1/types"
issuance "github.com/0glabs/0g-chain/x/issuance"
issuancekeeper "github.com/0glabs/0g-chain/x/issuance/keeper"
issuancetypes "github.com/0glabs/0g-chain/x/issuance/types"
pricefeed "github.com/0glabs/0g-chain/x/pricefeed"
pricefeedkeeper "github.com/0glabs/0g-chain/x/pricefeed/keeper"
pricefeedtypes "github.com/0glabs/0g-chain/x/pricefeed/types"
validatorvesting "github.com/0glabs/0g-chain/x/validator-vesting" validatorvesting "github.com/0glabs/0g-chain/x/validator-vesting"
validatorvestingrest "github.com/0glabs/0g-chain/x/validator-vesting/client/rest" validatorvestingrest "github.com/0glabs/0g-chain/x/validator-vesting/client/rest"
validatorvestingtypes "github.com/0glabs/0g-chain/x/validator-vesting/types" validatorvestingtypes "github.com/0glabs/0g-chain/x/validator-vesting/types"
@ -135,6 +147,7 @@ var (
upgradeclient.LegacyCancelProposalHandler, upgradeclient.LegacyCancelProposalHandler,
ibcclientclient.UpdateClientProposalHandler, ibcclientclient.UpdateClientProposalHandler,
ibcclientclient.UpgradeProposalHandler, ibcclientclient.UpgradeProposalHandler,
committeeclient.ProposalHandler,
}), }),
params.AppModuleBasic{}, params.AppModuleBasic{},
crisis.AppModuleBasic{}, crisis.AppModuleBasic{},
@ -154,6 +167,10 @@ var (
evmutil.AppModuleBasic{}, evmutil.AppModuleBasic{},
mint.AppModuleBasic{}, mint.AppModuleBasic{},
consensus.AppModuleBasic{}, consensus.AppModuleBasic{},
issuance.AppModuleBasic{},
bep3.AppModuleBasic{},
pricefeed.AppModuleBasic{},
committee.AppModuleBasic{},
council.AppModuleBasic{}, council.AppModuleBasic{},
das.AppModuleBasic{}, das.AppModuleBasic{},
) )
@ -171,6 +188,8 @@ var (
evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // used for secure addition and subtraction of balance using module account evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // used for secure addition and subtraction of balance using module account
evmutiltypes.ModuleName: {authtypes.Minter, authtypes.Burner}, evmutiltypes.ModuleName: {authtypes.Minter, authtypes.Burner},
minttypes.ModuleName: {authtypes.Minter}, minttypes.ModuleName: {authtypes.Minter},
issuancetypes.ModuleAccountName: {authtypes.Minter, authtypes.Burner},
bep3types.ModuleName: {authtypes.Burner, authtypes.Minter},
} }
) )
@ -234,6 +253,10 @@ type App struct {
consensusParamsKeeper consensusparamkeeper.Keeper consensusParamsKeeper consensusparamkeeper.Keeper
CouncilKeeper councilkeeper.Keeper CouncilKeeper councilkeeper.Keeper
DasKeeper daskeeper.Keeper DasKeeper daskeeper.Keeper
issuanceKeeper issuancekeeper.Keeper
bep3Keeper bep3keeper.Keeper
pricefeedKeeper pricefeedkeeper.Keeper
committeeKeeper committeekeeper.Keeper
// make scoped keepers public for test purposes // make scoped keepers public for test purposes
ScopedIBCKeeper capabilitykeeper.ScopedKeeper ScopedIBCKeeper capabilitykeeper.ScopedKeeper
@ -291,6 +314,10 @@ func NewApp(
crisistypes.StoreKey, crisistypes.StoreKey,
counciltypes.StoreKey, counciltypes.StoreKey,
dastypes.StoreKey, dastypes.StoreKey,
issuancetypes.StoreKey,
bep3types.StoreKey,
pricefeedtypes.StoreKey,
committeetypes.StoreKey,
) )
tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey, evmtypes.TransientKey, feemarkettypes.TransientKey) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey, evmtypes.TransientKey, feemarkettypes.TransientKey)
memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey)
@ -323,6 +350,9 @@ func NewApp(
slashingSubspace := app.paramsKeeper.Subspace(slashingtypes.ModuleName) slashingSubspace := app.paramsKeeper.Subspace(slashingtypes.ModuleName)
govSubspace := app.paramsKeeper.Subspace(govtypes.ModuleName).WithKeyTable(govv1.ParamKeyTable()) govSubspace := app.paramsKeeper.Subspace(govtypes.ModuleName).WithKeyTable(govv1.ParamKeyTable())
crisisSubspace := app.paramsKeeper.Subspace(crisistypes.ModuleName) crisisSubspace := app.paramsKeeper.Subspace(crisistypes.ModuleName)
issuanceSubspace := app.paramsKeeper.Subspace(issuancetypes.ModuleName)
bep3Subspace := app.paramsKeeper.Subspace(bep3types.ModuleName)
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeedtypes.ModuleName)
ibcSubspace := app.paramsKeeper.Subspace(ibcexported.ModuleName) ibcSubspace := app.paramsKeeper.Subspace(ibcexported.ModuleName)
ibctransferSubspace := app.paramsKeeper.Subspace(ibctransfertypes.ModuleName) ibctransferSubspace := app.paramsKeeper.Subspace(ibctransfertypes.ModuleName)
packetforwardSubspace := app.paramsKeeper.Subspace(packetforwardtypes.ModuleName).WithKeyTable(packetforwardtypes.ParamKeyTable()) packetforwardSubspace := app.paramsKeeper.Subspace(packetforwardtypes.ModuleName).WithKeyTable(packetforwardtypes.ParamKeyTable())
@ -491,6 +521,26 @@ func NewApp(
ibcRouter := ibcporttypes.NewRouter() ibcRouter := ibcporttypes.NewRouter()
ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack) ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack)
app.ibcKeeper.SetRouter(ibcRouter) app.ibcKeeper.SetRouter(ibcRouter)
app.issuanceKeeper = issuancekeeper.NewKeeper(
appCodec,
keys[issuancetypes.StoreKey],
issuanceSubspace,
app.accountKeeper,
app.bankKeeper,
)
app.bep3Keeper = bep3keeper.NewKeeper(
appCodec,
keys[bep3types.StoreKey],
app.bankKeeper,
app.accountKeeper,
bep3Subspace,
app.ModuleAccountAddrs(),
)
app.pricefeedKeeper = pricefeedkeeper.NewKeeper(
appCodec,
keys[pricefeedtypes.StoreKey],
pricefeedSubspace,
)
app.mintKeeper = mintkeeper.NewKeeper( app.mintKeeper = mintkeeper.NewKeeper(
appCodec, appCodec,
@ -501,7 +551,16 @@ func NewApp(
authtypes.FeeCollectorName, authtypes.FeeCollectorName,
govAuthAddrStr, govAuthAddrStr,
) )
// create committee keeper with router
committeeGovRouter := govv1beta1.NewRouter()
app.committeeKeeper = committeekeeper.NewKeeper(
appCodec,
keys[committeetypes.StoreKey],
committeeGovRouter,
app.paramsKeeper,
app.accountKeeper,
app.bankKeeper,
)
// register the staking hooks // register the staking hooks
app.stakingKeeper.SetHooks( app.stakingKeeper.SetHooks(
stakingtypes.NewMultiStakingHooks( stakingtypes.NewMultiStakingHooks(
@ -516,7 +575,8 @@ func NewApp(
AddRoute(govtypes.RouterKey, govv1beta1.ProposalHandler). AddRoute(govtypes.RouterKey, govv1beta1.ProposalHandler).
AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(&app.upgradeKeeper)). AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(&app.upgradeKeeper)).
AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.ibcKeeper.ClientKeeper)) AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.ibcKeeper.ClientKeeper)).
AddRoute(committeetypes.RouterKey, committee.NewProposalHandler(app.committeeKeeper))
govConfig := govtypes.DefaultConfig() govConfig := govtypes.DefaultConfig()
govKeeper := govkeeper.NewKeeper( govKeeper := govkeeper.NewKeeper(
@ -560,6 +620,10 @@ func NewApp(
transferModule, transferModule,
vesting.NewAppModule(app.accountKeeper, app.bankKeeper), vesting.NewAppModule(app.accountKeeper, app.bankKeeper),
authzmodule.NewAppModule(appCodec, app.authzKeeper, app.accountKeeper, app.bankKeeper, app.interfaceRegistry), authzmodule.NewAppModule(appCodec, app.authzKeeper, app.accountKeeper, app.bankKeeper, app.interfaceRegistry),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.bankKeeper),
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.bankKeeper),
pricefeed.NewAppModule(app.pricefeedKeeper, app.accountKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
validatorvesting.NewAppModule(app.bankKeeper), validatorvesting.NewAppModule(app.bankKeeper),
evmutil.NewAppModule(app.evmutilKeeper, app.bankKeeper, app.accountKeeper), evmutil.NewAppModule(app.evmutilKeeper, app.bankKeeper, app.accountKeeper),
mint.NewAppModule(appCodec, app.mintKeeper, app.accountKeeper, nil, mintSubspace), mint.NewAppModule(appCodec, app.mintKeeper, app.accountKeeper, nil, mintSubspace),
@ -571,6 +635,9 @@ func NewApp(
upgradetypes.ModuleName, upgradetypes.ModuleName,
// Capability begin blocker runs non state changing initialization. // Capability begin blocker runs non state changing initialization.
capabilitytypes.ModuleName, capabilitytypes.ModuleName,
// Committee begin blocker changes module params by enacting proposals.
// Run before to ensure params are updated together before state changes.
committeetypes.ModuleName,
minttypes.ModuleName, minttypes.ModuleName,
distrtypes.ModuleName, distrtypes.ModuleName,
// During begin block slashing happens after distr.BeginBlocker so that // During begin block slashing happens after distr.BeginBlocker so that
@ -581,6 +648,9 @@ func NewApp(
stakingtypes.ModuleName, stakingtypes.ModuleName,
feemarkettypes.ModuleName, feemarkettypes.ModuleName,
evmtypes.ModuleName, evmtypes.ModuleName,
bep3types.ModuleName,
issuancetypes.ModuleName,
pricefeedtypes.ModuleName,
ibcexported.ModuleName, ibcexported.ModuleName,
// Add all remaining modules with an empty begin blocker below since cosmos 0.45.0 requires it // Add all remaining modules with an empty begin blocker below since cosmos 0.45.0 requires it
vestingtypes.ModuleName, vestingtypes.ModuleName,
@ -609,10 +679,14 @@ func NewApp(
evmtypes.ModuleName, evmtypes.ModuleName,
// fee market module must go after evm module in order to retrieve the block gas used. // fee market module must go after evm module in order to retrieve the block gas used.
feemarkettypes.ModuleName, feemarkettypes.ModuleName,
pricefeedtypes.ModuleName,
// Add all remaining modules with an empty end blocker below since cosmos 0.45.0 requires it // Add all remaining modules with an empty end blocker below since cosmos 0.45.0 requires it
capabilitytypes.ModuleName, capabilitytypes.ModuleName,
issuancetypes.ModuleName,
slashingtypes.ModuleName, slashingtypes.ModuleName,
distrtypes.ModuleName, distrtypes.ModuleName,
bep3types.ModuleName,
committeetypes.ModuleName,
upgradetypes.ModuleName, upgradetypes.ModuleName,
evidencetypes.ModuleName, evidencetypes.ModuleName,
vestingtypes.ModuleName, vestingtypes.ModuleName,
@ -648,6 +722,10 @@ func NewApp(
ibctransfertypes.ModuleName, ibctransfertypes.ModuleName,
evmtypes.ModuleName, evmtypes.ModuleName,
feemarkettypes.ModuleName, feemarkettypes.ModuleName,
issuancetypes.ModuleName,
bep3types.ModuleName,
pricefeedtypes.ModuleName,
committeetypes.ModuleName,
evmutiltypes.ModuleName, evmutiltypes.ModuleName,
genutiltypes.ModuleName, // runs arbitrary txs included in genisis state, so run after modules have been initialized genutiltypes.ModuleName, // runs arbitrary txs included in genisis state, so run after modules have been initialized
// Add all remaining modules with an empty InitGenesis below since cosmos 0.45.0 requires it // Add all remaining modules with an empty InitGenesis below since cosmos 0.45.0 requires it
@ -698,6 +776,8 @@ func NewApp(
if options.MempoolEnableAuth { if options.MempoolEnableAuth {
fetchers = append(fetchers, fetchers = append(fetchers,
func(sdk.Context) []sdk.AccAddress { return options.MempoolAuthAddresses }, func(sdk.Context) []sdk.AccAddress { return options.MempoolAuthAddresses },
app.bep3Keeper.GetAuthorizedAddresses,
app.pricefeedKeeper.GetAuthorizedAddresses,
) )
} }

View File

@ -42,7 +42,11 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/0glabs/0g-chain/chaincfg" "github.com/0glabs/0g-chain/chaincfg"
bep3keeper "github.com/0glabs/0g-chain/x/bep3/keeper"
committeekeeper "github.com/0glabs/0g-chain/x/committee/keeper"
evmutilkeeper "github.com/0glabs/0g-chain/x/evmutil/keeper" evmutilkeeper "github.com/0glabs/0g-chain/x/evmutil/keeper"
issuancekeeper "github.com/0glabs/0g-chain/x/issuance/keeper"
pricefeedkeeper "github.com/0glabs/0g-chain/x/pricefeed/keeper"
) )
var ( var (
@ -103,7 +107,10 @@ func (tApp TestApp) GetDistrKeeper() distkeeper.Keeper { return tApp.di
func (tApp TestApp) GetGovKeeper() govkeeper.Keeper { return tApp.govKeeper } func (tApp TestApp) GetGovKeeper() govkeeper.Keeper { return tApp.govKeeper }
func (tApp TestApp) GetCrisisKeeper() crisiskeeper.Keeper { return tApp.crisisKeeper } func (tApp TestApp) GetCrisisKeeper() crisiskeeper.Keeper { return tApp.crisisKeeper }
func (tApp TestApp) GetParamsKeeper() paramskeeper.Keeper { return tApp.paramsKeeper } func (tApp TestApp) GetParamsKeeper() paramskeeper.Keeper { return tApp.paramsKeeper }
func (tApp TestApp) GetIssuanceKeeper() issuancekeeper.Keeper { return tApp.issuanceKeeper }
func (tApp TestApp) GetBep3Keeper() bep3keeper.Keeper { return tApp.bep3Keeper }
func (tApp TestApp) GetPriceFeedKeeper() pricefeedkeeper.Keeper { return tApp.pricefeedKeeper }
func (tApp TestApp) GetCommitteeKeeper() committeekeeper.Keeper { return tApp.committeeKeeper }
func (tApp TestApp) GetEvmutilKeeper() evmutilkeeper.Keeper { return tApp.evmutilKeeper } func (tApp TestApp) GetEvmutilKeeper() evmutilkeeper.Keeper { return tApp.evmutilKeeper }
func (tApp TestApp) GetEvmKeeper() *evmkeeper.Keeper { return tApp.evmKeeper } func (tApp TestApp) GetEvmKeeper() *evmkeeper.Keeper { return tApp.evmKeeper }

Binary file not shown.

View File

@ -0,0 +1,160 @@
syntax = "proto3";
package zgc.bep3.v1beta1;
import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/duration.proto";
option go_package = "github.com/0glabs/0g-chain/x/bep3/types";
// Params defines the parameters for the bep3 module.
message Params {
// asset_params define the parameters for each bep3 asset
repeated AssetParam asset_params = 1 [
(gogoproto.castrepeated) = "AssetParams",
(gogoproto.nullable) = false
];
}
// AssetParam defines parameters for each bep3 asset.
message AssetParam {
// denom represents the denominatin for this asset
string denom = 1;
// coin_id represents the registered coin type to use (https://github.com/satoshilabs/slips/blob/master/slip-0044.md)
int64 coin_id = 2 [(gogoproto.customname) = "CoinID"];
// supply_limit defines the maximum supply allowed for the asset - a total or time based rate limit
SupplyLimit supply_limit = 3 [(gogoproto.nullable) = false];
// active specifies if the asset is live or paused
bool active = 4;
// deputy_address the 0g-chain address of the deputy
bytes deputy_address = 5 [
(cosmos_proto.scalar) = "cosmos.AddressBytes",
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
];
// fixed_fee defines the fee for incoming swaps
string fixed_fee = 6 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
// min_swap_amount defines the minimum amount able to be swapped in a single message
string min_swap_amount = 7 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
// max_swap_amount defines the maximum amount able to be swapped in a single message
string max_swap_amount = 8 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
// min_block_lock defined the minimum blocks to lock
uint64 min_block_lock = 9;
// min_block_lock defined the maximum blocks to lock
uint64 max_block_lock = 10;
}
// SupplyLimit define the absolute and time-based limits for an assets's supply.
message SupplyLimit {
// limit defines the total supply allowed
string limit = 1 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
// time_limited enables or disables time based supply limiting
bool time_limited = 2;
// time_period specifies the duration that time_based_limit is evalulated
google.protobuf.Duration time_period = 3 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
// time_based_limit defines the maximum supply that can be swapped within time_period
string time_based_limit = 4 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
}
// SwapStatus is the status of an AtomicSwap
enum SwapStatus {
option (gogoproto.goproto_enum_prefix) = false;
// SWAP_STATUS_UNSPECIFIED represents an unspecified status
SWAP_STATUS_UNSPECIFIED = 0;
// SWAP_STATUS_OPEN represents an open swap
SWAP_STATUS_OPEN = 1;
// SWAP_STATUS_COMPLETED represents a completed swap
SWAP_STATUS_COMPLETED = 2;
// SWAP_STATUS_EXPIRED represents an expired swap
SWAP_STATUS_EXPIRED = 3;
}
// SwapDirection is the direction of an AtomicSwap
enum SwapDirection {
option (gogoproto.goproto_enum_prefix) = false;
// SWAP_DIRECTION_UNSPECIFIED represents unspecified or invalid swap direcation
SWAP_DIRECTION_UNSPECIFIED = 0;
// SWAP_DIRECTION_INCOMING represents is incoming swap (to the 0g-chain)
SWAP_DIRECTION_INCOMING = 1;
// SWAP_DIRECTION_OUTGOING represents an outgoing swap (from the 0g- chain)
SWAP_DIRECTION_OUTGOING = 2;
}
// AtomicSwap defines an atomic swap between chains for the pricefeed module.
message AtomicSwap {
// amount represents the amount being swapped
repeated cosmos.base.v1beta1.Coin amount = 1 [
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];
// random_number_hash represents the hash of the random number
bytes random_number_hash = 2 [(gogoproto.casttype) = "github.com/cometbft/cometbft/libs/bytes.HexBytes"];
// expire_height represents the height when the swap expires
uint64 expire_height = 3;
// timestamp represents the timestamp of the swap
int64 timestamp = 4;
// sender is the 0g-chain sender of the swap
bytes sender = 5 [
(cosmos_proto.scalar) = "cosmos.AddressBytes",
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
];
// recipient is the 0g-chain recipient of the swap
bytes recipient = 6 [
(cosmos_proto.scalar) = "cosmos.AddressBytes",
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
];
// sender_other_chain is the sender on the other chain
string sender_other_chain = 7;
// recipient_other_chain is the recipient on the other chain
string recipient_other_chain = 8;
// closed_block is the block when the swap is closed
int64 closed_block = 9;
// status represents the current status of the swap
SwapStatus status = 10;
// cross_chain identifies whether the atomic swap is cross chain
bool cross_chain = 11;
// direction identifies if the swap is incoming or outgoing
SwapDirection direction = 12;
}
// AssetSupply defines information about an asset's supply.
message AssetSupply {
// incoming_supply represents the incoming supply of an asset
cosmos.base.v1beta1.Coin incoming_supply = 1 [(gogoproto.nullable) = false];
// outgoing_supply represents the outgoing supply of an asset
cosmos.base.v1beta1.Coin outgoing_supply = 2 [(gogoproto.nullable) = false];
// current_supply represents the current on-chain supply of an asset
cosmos.base.v1beta1.Coin current_supply = 3 [(gogoproto.nullable) = false];
// time_limited_current_supply represents the time limited current supply of an asset
cosmos.base.v1beta1.Coin time_limited_current_supply = 4 [(gogoproto.nullable) = false];
// time_elapsed represents the time elapsed
google.protobuf.Duration time_elapsed = 5 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
}

View File

@ -0,0 +1,32 @@
syntax = "proto3";
package zgc.bep3.v1beta1;
import "gogoproto/gogo.proto";
import "google/protobuf/timestamp.proto";
import "zgc/bep3/v1beta1/bep3.proto";
option go_package = "github.com/0glabs/0g-chain/x/bep3/types";
// GenesisState defines the pricefeed module's genesis state.
message GenesisState {
// params defines all the parameters of the module.
Params params = 1 [(gogoproto.nullable) = false];
// atomic_swaps represents the state of stored atomic swaps
repeated AtomicSwap atomic_swaps = 2 [
(gogoproto.castrepeated) = "AtomicSwaps",
(gogoproto.nullable) = false
];
// supplies represents the supply information of each atomic swap
repeated AssetSupply supplies = 3 [
(gogoproto.castrepeated) = "AssetSupplies",
(gogoproto.nullable) = false
];
// previous_block_time represents the time of the previous block
google.protobuf.Timestamp previous_block_time = 4 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false
];
}

View File

@ -0,0 +1,165 @@
syntax = "proto3";
package zgc.bep3.v1beta1;
import "cosmos/base/query/v1beta1/pagination.proto";
import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "google/protobuf/duration.proto";
import "zgc/bep3/v1beta1/bep3.proto";
option go_package = "github.com/0glabs/0g-chain/x/bep3/types";
// Query defines the gRPC querier service for bep3 module
service Query {
// Params queries module params
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/0g-chain/bep3/v1beta1/params";
}
// AssetSupply queries info about an asset's supply
rpc AssetSupply(QueryAssetSupplyRequest) returns (QueryAssetSupplyResponse) {
option (google.api.http).get = "/0g-chain/bep3/v1beta1/assetsupply/{denom}";
}
// AssetSupplies queries a list of asset supplies
rpc AssetSupplies(QueryAssetSuppliesRequest) returns (QueryAssetSuppliesResponse) {
option (google.api.http).get = "/0g-chain/bep3/v1beta1/assetsupplies";
}
// AtomicSwap queries info about an atomic swap
rpc AtomicSwap(QueryAtomicSwapRequest) returns (QueryAtomicSwapResponse) {
option (google.api.http).get = "/0g-chain/bep3/v1beta1/atomicswap/{swap_id}";
}
// AtomicSwaps queries a list of atomic swaps
rpc AtomicSwaps(QueryAtomicSwapsRequest) returns (QueryAtomicSwapsResponse) {
option (google.api.http).get = "/0g-chain/bep3/v1beta1/atomicswaps";
}
}
// QueryParamsRequest defines the request type for querying x/bep3 parameters.
message QueryParamsRequest {}
// QueryParamsResponse defines the response type for querying x/bep3 parameters.
message QueryParamsResponse {
// params represents the parameters of the module
Params params = 1 [(gogoproto.nullable) = false];
}
// QueryAssetSupplyRequest is the request type for the Query/AssetSupply RPC method.
message QueryAssetSupplyRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
// denom filters the asset response for the specified denom
string denom = 1;
}
// AssetSupplyResponse defines information about an asset's supply.
message AssetSupplyResponse {
// incoming_supply represents the incoming supply of an asset
cosmos.base.v1beta1.Coin incoming_supply = 1 [(gogoproto.nullable) = false];
// outgoing_supply represents the outgoing supply of an asset
cosmos.base.v1beta1.Coin outgoing_supply = 2 [(gogoproto.nullable) = false];
// current_supply represents the current on-chain supply of an asset
cosmos.base.v1beta1.Coin current_supply = 3 [(gogoproto.nullable) = false];
// time_limited_current_supply represents the time limited current supply of an asset
cosmos.base.v1beta1.Coin time_limited_current_supply = 4 [(gogoproto.nullable) = false];
// time_elapsed represents the time elapsed
google.protobuf.Duration time_elapsed = 5 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
}
// QueryAssetSupplyResponse is the response type for the Query/AssetSupply RPC method.
message QueryAssetSupplyResponse {
// asset_supply represents the supply of the asset
AssetSupplyResponse asset_supply = 1 [(gogoproto.nullable) = false];
}
// QueryAssetSuppliesRequest is the request type for the Query/AssetSupplies RPC method.
message QueryAssetSuppliesRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
}
// QueryAssetSuppliesResponse is the response type for the Query/AssetSupplies RPC method.
message QueryAssetSuppliesResponse {
// asset_supplies represents the supplies of returned assets
repeated AssetSupplyResponse asset_supplies = 1 [(gogoproto.nullable) = false];
}
// QueryAtomicSwapRequest is the request type for the Query/AtomicSwap RPC method.
message QueryAtomicSwapRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
// swap_id represents the id of the swap to query
string swap_id = 1;
}
// QueryAtomicSwapResponse is the response type for the Query/AtomicSwap RPC method.
message QueryAtomicSwapResponse {
AtomicSwapResponse atomic_swap = 2 [(gogoproto.nullable) = false];
}
// AtomicSwapResponse represents the returned atomic swap properties
message AtomicSwapResponse {
// id represents the id of the atomic swap
string id = 1;
// amount represents the amount being swapped
repeated cosmos.base.v1beta1.Coin amount = 2 [
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];
// random_number_hash represents the hash of the random number
string random_number_hash = 3;
// expire_height represents the height when the swap expires
uint64 expire_height = 4;
// timestamp represents the timestamp of the swap
int64 timestamp = 5;
// sender is the 0g-chain sender of the swap
string sender = 6 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// recipient is the 0g-chain recipient of the swap
string recipient = 7 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// sender_other_chain is the sender on the other chain
string sender_other_chain = 8;
// recipient_other_chain is the recipient on the other chain
string recipient_other_chain = 9;
// closed_block is the block when the swap is closed
int64 closed_block = 10;
// status represents the current status of the swap
SwapStatus status = 11;
// cross_chain identifies whether the atomic swap is cross chain
bool cross_chain = 12;
// direction identifies if the swap is incoming or outgoing
SwapDirection direction = 13;
}
// QueryAtomicSwapsRequest is the request type for the Query/AtomicSwaps RPC method.
message QueryAtomicSwapsRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
// involve filters by address
string involve = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// expiration filters by expiration block height
uint64 expiration = 2;
// status filters by swap status
SwapStatus status = 3;
// direction fitlers by swap direction
SwapDirection direction = 4;
cosmos.base.query.v1beta1.PageRequest pagination = 5;
}
// QueryAtomicSwapsResponse is the response type for the Query/AtomicSwaps RPC method.
message QueryAtomicSwapsResponse {
// atomic_swap represents the returned atomic swaps for the request
repeated AtomicSwapResponse atomic_swaps = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 3;
}

View File

@ -0,0 +1,69 @@
syntax = "proto3";
package zgc.bep3.v1beta1;
import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
option go_package = "github.com/0glabs/0g-chain/x/bep3/types";
// Msg defines the bep3 Msg service.
service Msg {
// CreateAtomicSwap defines a method for creating an atomic swap
rpc CreateAtomicSwap(MsgCreateAtomicSwap) returns (MsgCreateAtomicSwapResponse);
// ClaimAtomicSwap defines a method for claiming an atomic swap
rpc ClaimAtomicSwap(MsgClaimAtomicSwap) returns (MsgClaimAtomicSwapResponse);
// RefundAtomicSwap defines a method for refunding an atomic swap
rpc RefundAtomicSwap(MsgRefundAtomicSwap) returns (MsgRefundAtomicSwapResponse);
}
// MsgCreateAtomicSwap defines the Msg/CreateAtomicSwap request type.
message MsgCreateAtomicSwap {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string to = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string recipient_other_chain = 3;
string sender_other_chain = 4;
string random_number_hash = 5;
int64 timestamp = 6;
repeated cosmos.base.v1beta1.Coin amount = 7 [
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];
uint64 height_span = 8;
}
// MsgCreateAtomicSwapResponse defines the Msg/CreateAtomicSwap response type.
message MsgCreateAtomicSwapResponse {}
// MsgClaimAtomicSwap defines the Msg/ClaimAtomicSwap request type.
message MsgClaimAtomicSwap {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string swap_id = 2 [(gogoproto.customname) = "SwapID"];
string random_number = 3;
}
// MsgClaimAtomicSwapResponse defines the Msg/ClaimAtomicSwap response type.
message MsgClaimAtomicSwapResponse {}
// MsgRefundAtomicSwap defines the Msg/RefundAtomicSwap request type.
message MsgRefundAtomicSwap {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string swap_id = 2 [(gogoproto.customname) = "SwapID"];
}
// MsgRefundAtomicSwapResponse defines the Msg/RefundAtomicSwap response type.
message MsgRefundAtomicSwapResponse {}

View File

@ -0,0 +1,70 @@
syntax = "proto3";
package zgc.committee.v1beta1;
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "google/protobuf/duration.proto";
option go_package = "github.com/0glabs/0g-chain/x/committee/types";
option (gogoproto.goproto_getters_all) = false;
// BaseCommittee is a common type shared by all Committees
message BaseCommittee {
option (cosmos_proto.implements_interface) = "Committee";
option (gogoproto.goproto_stringer) = false;
uint64 id = 1 [(gogoproto.customname) = "ID"];
string description = 2;
repeated bytes members = 3 [
(cosmos_proto.scalar) = "cosmos.AddressBytes",
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
];
repeated google.protobuf.Any permissions = 4 [(cosmos_proto.accepts_interface) = "Permission"];
// Smallest percentage that must vote for a proposal to pass
string vote_threshold = 5 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// The length of time a proposal remains active for. Proposals will close earlier if they get enough votes.
google.protobuf.Duration proposal_duration = 6 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
TallyOption tally_option = 7;
}
// MemberCommittee is an alias of BaseCommittee
message MemberCommittee {
option (cosmos_proto.implements_interface) = "Committee";
option (gogoproto.goproto_stringer) = false;
BaseCommittee base_committee = 1 [(gogoproto.embed) = true];
}
// TokenCommittee supports voting on proposals by token holders
message TokenCommittee {
option (cosmos_proto.implements_interface) = "Committee";
option (gogoproto.goproto_stringer) = false;
BaseCommittee base_committee = 1 [(gogoproto.embed) = true];
string quorum = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string tally_denom = 3;
}
// TallyOption enumerates the valid types of a tally.
enum TallyOption {
option (gogoproto.goproto_enum_prefix) = false;
// TALLY_OPTION_UNSPECIFIED defines a null tally option.
TALLY_OPTION_UNSPECIFIED = 0;
// Votes are tallied each block and the proposal passes as soon as the vote threshold is reached
TALLY_OPTION_FIRST_PAST_THE_POST = 1;
// Votes are tallied exactly once, when the deadline time is reached
TALLY_OPTION_DEADLINE = 2;
}

View File

@ -0,0 +1,62 @@
syntax = "proto3";
package zgc.committee.v1beta1;
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/0glabs/0g-chain/x/committee/types";
// GenesisState defines the committee module's genesis state.
message GenesisState {
option (gogoproto.goproto_getters) = false;
uint64 next_proposal_id = 1 [(gogoproto.customname) = "NextProposalID"];
repeated google.protobuf.Any committees = 2 [(cosmos_proto.accepts_interface) = "Committee"];
repeated Proposal proposals = 3 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "Proposals"
];
repeated Vote votes = 4 [(gogoproto.nullable) = false];
}
// Proposal is an internal record of a governance proposal submitted to a committee.
message Proposal {
option (gogoproto.goproto_getters) = false;
option (gogoproto.goproto_stringer) = false;
google.protobuf.Any content = 1 [(cosmos_proto.accepts_interface) = "cosmos.gov.v1beta1.Content"];
uint64 id = 2 [(gogoproto.customname) = "ID"];
uint64 committee_id = 3 [(gogoproto.customname) = "CommitteeID"];
google.protobuf.Timestamp deadline = 4 [
(gogoproto.nullable) = false,
(gogoproto.stdtime) = true
];
}
// Vote is an internal record of a single governance vote.
message Vote {
option (gogoproto.goproto_getters) = false;
uint64 proposal_id = 1 [(gogoproto.customname) = "ProposalID"];
bytes voter = 2 [
(cosmos_proto.scalar) = "cosmos.AddressBytes",
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
];
VoteType vote_type = 3;
}
// VoteType enumerates the valid types of a vote.
enum VoteType {
option (gogoproto.goproto_enum_prefix) = false;
// VOTE_TYPE_UNSPECIFIED defines a no-op vote option.
VOTE_TYPE_UNSPECIFIED = 0;
// VOTE_TYPE_YES defines a yes vote option.
VOTE_TYPE_YES = 1;
// VOTE_TYPE_NO defines a no vote option.
VOTE_TYPE_NO = 2;
// VOTE_TYPE_ABSTAIN defines an abstain vote option.
VOTE_TYPE_ABSTAIN = 3;
}

View File

@ -0,0 +1,72 @@
syntax = "proto3";
package zgc.committee.v1beta1;
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
option go_package = "github.com/0glabs/0g-chain/x/committee/types";
// GodPermission allows any governance proposal. It is used mainly for testing.
message GodPermission {
option (cosmos_proto.implements_interface) = "Permission";
}
// SoftwareUpgradePermission permission type for software upgrade proposals
message SoftwareUpgradePermission {
option (cosmos_proto.implements_interface) = "Permission";
}
// TextPermission allows any text governance proposal.
message TextPermission {
option (cosmos_proto.implements_interface) = "Permission";
}
// CommunityCDPRepayDebtPermission allows submission of CommunityCDPRepayDebtProposal
message CommunityCDPRepayDebtPermission {
option (cosmos_proto.implements_interface) = "Permission";
}
// CommunityCDPWithdrawCollateralPermission allows submission of CommunityCDPWithdrawCollateralProposal
message CommunityCDPWithdrawCollateralPermission {
option (cosmos_proto.implements_interface) = "Permission";
}
// CommunityPoolLendWithdrawPermission allows submission of CommunityPoolLendWithdrawProposal
message CommunityPoolLendWithdrawPermission {
option (cosmos_proto.implements_interface) = "Permission";
}
// ParamsChangePermission allows any parameter or sub parameter change proposal.
message ParamsChangePermission {
option (cosmos_proto.implements_interface) = "Permission";
repeated AllowedParamsChange allowed_params_changes = 1 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "AllowedParamsChanges"
];
}
// AllowedParamsChange contains data on the allowed parameter changes for subspace, key, and sub params requirements.
message AllowedParamsChange {
string subspace = 1;
string key = 2;
// Requirements for when the subparam value is a single record. This contains list of allowed attribute keys that can
// be changed on the subparam record.
repeated string single_subparam_allowed_attrs = 3;
// Requirements for when the subparam value is a list of records. The requirements contains requirements for each
// record in the list.
repeated SubparamRequirement multi_subparams_requirements = 4 [(gogoproto.nullable) = false];
}
// SubparamRequirement contains requirements for a single record in a subparam value list
message SubparamRequirement {
// The required attr key of the param record.
string key = 1;
// The required param value for the param record key. The key and value is used to match to the target param record.
string val = 2;
// The sub param attrs that are allowed to be changed.
repeated string allowed_subparam_attr_changes = 3;
}

View File

@ -0,0 +1,27 @@
syntax = "proto3";
package zgc.committee.v1beta1;
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
option go_package = "github.com/0glabs/0g-chain/x/committee/types";
option (gogoproto.goproto_getters_all) = false;
// CommitteeChangeProposal is a gov proposal for creating a new committee or modifying an existing one.
message CommitteeChangeProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
string title = 1;
string description = 2;
google.protobuf.Any new_committee = 3 [(cosmos_proto.accepts_interface) = "Committee"];
}
// CommitteeDeleteProposal is a gov proposal for removing a committee.
message CommitteeDeleteProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
string title = 1;
string description = 2;
uint64 committee_id = 3 [(gogoproto.customname) = "CommitteeID"];
}

View File

@ -0,0 +1,181 @@
syntax = "proto3";
package zgc.committee.v1beta1;
import "cosmos/base/query/v1beta1/pagination.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
import "zgc/committee/v1beta1/genesis.proto";
option go_package = "github.com/0glabs/0g-chain/x/committee/types";
option (gogoproto.goproto_getters_all) = false;
// Query defines the gRPC querier service for committee module
service Query {
// Committees queries all committess of the committee module.
rpc Committees(QueryCommitteesRequest) returns (QueryCommitteesResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/committees";
}
// Committee queries a committee based on committee ID.
rpc Committee(QueryCommitteeRequest) returns (QueryCommitteeResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/committees/{committee_id}";
}
// Proposals queries proposals based on committee ID.
rpc Proposals(QueryProposalsRequest) returns (QueryProposalsResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/proposals";
}
// Deposits queries a proposal based on proposal ID.
rpc Proposal(QueryProposalRequest) returns (QueryProposalResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/proposals/{proposal_id}";
}
// NextProposalID queries the next proposal ID of the committee module.
rpc NextProposalID(QueryNextProposalIDRequest) returns (QueryNextProposalIDResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/next-proposal-id";
}
// Votes queries all votes for a single proposal ID.
rpc Votes(QueryVotesRequest) returns (QueryVotesResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/proposals/{proposal_id}/votes";
}
// Vote queries the vote of a single voter for a single proposal ID.
rpc Vote(QueryVoteRequest) returns (QueryVoteResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/proposals/{proposal_id}/votes/{voter}";
}
// Tally queries the tally of a single proposal ID.
rpc Tally(QueryTallyRequest) returns (QueryTallyResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/proposals/{proposal_id}/tally";
}
// RawParams queries the raw params data of any subspace and key.
rpc RawParams(QueryRawParamsRequest) returns (QueryRawParamsResponse) {
option (google.api.http).get = "/0g-chain/committee/v1beta1/raw-params";
}
}
// QueryCommitteesRequest defines the request type for querying x/committee committees.
message QueryCommitteesRequest {}
// QueryCommitteesResponse defines the response type for querying x/committee committees.
message QueryCommitteesResponse {
repeated google.protobuf.Any committees = 1 [(cosmos_proto.accepts_interface) = "Committee"];
}
// QueryCommitteeRequest defines the request type for querying x/committee committee.
message QueryCommitteeRequest {
uint64 committee_id = 1;
}
// QueryCommitteeResponse defines the response type for querying x/committee committee.
message QueryCommitteeResponse {
google.protobuf.Any committee = 1 [(cosmos_proto.accepts_interface) = "Committee"];
}
// QueryProposalsRequest defines the request type for querying x/committee proposals.
message QueryProposalsRequest {
uint64 committee_id = 1;
}
// QueryProposalsResponse defines the response type for querying x/committee proposals.
message QueryProposalsResponse {
repeated QueryProposalResponse proposals = 1 [(gogoproto.nullable) = false];
}
// QueryProposalRequest defines the request type for querying x/committee proposal.
message QueryProposalRequest {
uint64 proposal_id = 1;
}
// QueryProposalResponse defines the response type for querying x/committee proposal.
message QueryProposalResponse {
google.protobuf.Any pub_proposal = 1 [
(cosmos_proto.accepts_interface) = "cosmos.gov.v1beta1.Content",
(gogoproto.customname) = "PubProposal"
];
uint64 id = 2 [(gogoproto.customname) = "ID"];
uint64 committee_id = 3 [(gogoproto.customname) = "CommitteeID"];
google.protobuf.Timestamp deadline = 4 [
(gogoproto.nullable) = false,
(gogoproto.stdtime) = true
];
}
// QueryNextProposalIDRequest defines the request type for querying x/committee NextProposalID.
message QueryNextProposalIDRequest {}
// QueryNextProposalIDRequest defines the response type for querying x/committee NextProposalID.
message QueryNextProposalIDResponse {
uint64 next_proposal_id = 1 [(gogoproto.customname) = "NextProposalID"];
}
// QueryVotesRequest defines the request type for querying x/committee votes.
message QueryVotesRequest {
uint64 proposal_id = 1;
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
// QueryVotesResponse defines the response type for querying x/committee votes.
message QueryVotesResponse {
// votes defined the queried votes.
repeated QueryVoteResponse votes = 1 [(gogoproto.nullable) = false];
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
// QueryVoteRequest defines the request type for querying x/committee vote.
message QueryVoteRequest {
uint64 proposal_id = 1;
string voter = 2;
}
// QueryVoteResponse defines the response type for querying x/committee vote.
message QueryVoteResponse {
uint64 proposal_id = 1 [(gogoproto.customname) = "ProposalID"];
string voter = 2;
VoteType vote_type = 3;
}
// QueryTallyRequest defines the request type for querying x/committee tally.
message QueryTallyRequest {
uint64 proposal_id = 1;
}
// QueryTallyResponse defines the response type for querying x/committee tally.
message QueryTallyResponse {
uint64 proposal_id = 1 [(gogoproto.customname) = "ProposalID"];
string yes_votes = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string no_votes = 3 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string current_votes = 4 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string possible_votes = 5 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string vote_threshold = 6 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string quorum = 7 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}
// QueryRawParamsRequest defines the request type for querying x/committee raw params.
message QueryRawParamsRequest {
string subspace = 1;
string key = 2;
}
// QueryRawParamsResponse defines the response type for querying x/committee raw params.
message QueryRawParamsResponse {
string raw_data = 1;
}

View File

@ -0,0 +1,40 @@
syntax = "proto3";
package zgc.committee.v1beta1;
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "zgc/committee/v1beta1/genesis.proto";
option go_package = "github.com/0glabs/0g-chain/x/committee/types";
option (gogoproto.goproto_getters_all) = false;
// Msg defines the committee Msg service
service Msg {
// SubmitProposal defines a method for submitting a committee proposal
rpc SubmitProposal(MsgSubmitProposal) returns (MsgSubmitProposalResponse);
// Vote defines a method for voting on a proposal
rpc Vote(MsgVote) returns (MsgVoteResponse);
}
// MsgSubmitProposal is used by committee members to create a new proposal that they can vote on.
message MsgSubmitProposal {
google.protobuf.Any pub_proposal = 1 [(cosmos_proto.accepts_interface) = "cosmos.gov.v1beta1.Content"];
string proposer = 2;
uint64 committee_id = 3 [(gogoproto.customname) = "CommitteeID"];
}
// MsgSubmitProposalResponse defines the SubmitProposal response type
message MsgSubmitProposalResponse {
uint64 proposal_id = 1 [(gogoproto.customname) = "ProposalID"];
}
// MsgVote is submitted by committee members to vote on proposals.
message MsgVote {
uint64 proposal_id = 1 [(gogoproto.customname) = "ProposalID"];
string voter = 2;
VoteType vote_type = 3;
}
// MsgVoteResponse defines the Vote response type
message MsgVoteResponse {}

View File

@ -0,0 +1,64 @@
syntax = "proto3";
package zgc.issuance.v1beta1;
import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/duration.proto";
option go_package = "github.com/0glabs/0g-chain/x/issuance/types";
// GenesisState defines the issuance module's genesis state.
message GenesisState {
// params defines all the parameters of the module.
Params params = 1 [(gogoproto.nullable) = false];
repeated AssetSupply supplies = 2 [(gogoproto.nullable) = false];
}
// Params defines the parameters for the issuance module.
message Params {
option (gogoproto.goproto_stringer) = false;
repeated Asset assets = 1 [(gogoproto.nullable) = false];
}
// Asset type for assets in the issuance module
message Asset {
option (gogoproto.goproto_stringer) = false;
string owner = 1;
string denom = 2;
repeated string blocked_addresses = 3;
bool paused = 4;
bool blockable = 5;
RateLimit rate_limit = 6 [(gogoproto.nullable) = false];
}
// RateLimit parameters for rate-limiting the supply of an issued asset
message RateLimit {
bool active = 1;
bytes limit = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "limit,omitempty"
];
google.protobuf.Duration time_period = 3 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
}
// AssetSupply contains information about an asset's rate-limited supply (the
// total supply of the asset is tracked in the top-level supply module)
message AssetSupply {
option (gogoproto.goproto_stringer) = false;
cosmos.base.v1beta1.Coin current_supply = 1 [(gogoproto.nullable) = false];
google.protobuf.Duration time_elapsed = 2 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
}

View File

@ -0,0 +1,24 @@
syntax = "proto3";
package zgc.issuance.v1beta1;
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "zgc/issuance/v1beta1/genesis.proto";
option go_package = "github.com/0glabs/0g-chain/x/issuance/types";
// Query defines the gRPC querier service for issuance module
service Query {
// Params queries all parameters of the issuance module.
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/0g-chain/issuance/v1beta1/params";
}
}
// QueryParamsRequest defines the request type for querying x/issuance parameters.
message QueryParamsRequest {}
// QueryParamsResponse defines the response type for querying x/issuance parameters.
message QueryParamsResponse {
Params params = 1 [(gogoproto.nullable) = false];
}

View File

@ -0,0 +1,89 @@
syntax = "proto3";
package zgc.issuance.v1beta1;
import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";
option go_package = "github.com/0glabs/0g-chain/x/issuance/types";
// Msg defines the issuance Msg service.
service Msg {
// IssueTokens message type used by the issuer to issue new tokens
rpc IssueTokens(MsgIssueTokens) returns (MsgIssueTokensResponse);
// RedeemTokens message type used by the issuer to redeem (burn) tokens
rpc RedeemTokens(MsgRedeemTokens) returns (MsgRedeemTokensResponse);
// BlockAddress message type used by the issuer to block an address from holding or transferring tokens
rpc BlockAddress(MsgBlockAddress) returns (MsgBlockAddressResponse);
// UnblockAddress message type used by the issuer to unblock an address from holding or transferring tokens
rpc UnblockAddress(MsgUnblockAddress) returns (MsgUnblockAddressResponse);
// SetPauseStatus message type used to pause or unpause status
rpc SetPauseStatus(MsgSetPauseStatus) returns (MsgSetPauseStatusResponse);
}
// MsgIssueTokens represents a message used by the issuer to issue new tokens
message MsgIssueTokens {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string sender = 1;
cosmos.base.v1beta1.Coin tokens = 2 [(gogoproto.nullable) = false];
string receiver = 3;
}
// MsgIssueTokensResponse defines the Msg/IssueTokens response type.
message MsgIssueTokensResponse {}
// MsgRedeemTokens represents a message used by the issuer to redeem (burn) tokens
message MsgRedeemTokens {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string sender = 1;
cosmos.base.v1beta1.Coin tokens = 2 [(gogoproto.nullable) = false];
}
// MsgRedeemTokensResponse defines the Msg/RedeemTokens response type.
message MsgRedeemTokensResponse {}
// MsgBlockAddress represents a message used by the issuer to block an address from holding or transferring tokens
message MsgBlockAddress {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string sender = 1;
string denom = 2;
string blocked_address = 3;
}
// MsgBlockAddressResponse defines the Msg/BlockAddress response type.
message MsgBlockAddressResponse {}
// MsgUnblockAddress message type used by the issuer to unblock an address from holding or transferring tokens
message MsgUnblockAddress {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string sender = 1;
string denom = 2;
string blocked_address = 3;
}
// MsgUnblockAddressResponse defines the Msg/UnblockAddress response type.
message MsgUnblockAddressResponse {}
// MsgSetPauseStatus message type used by the issuer to pause or unpause status
message MsgSetPauseStatus {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string sender = 1;
string denom = 2;
bool status = 3;
}
// MsgSetPauseStatusResponse defines the Msg/SetPauseStatus response type.
message MsgSetPauseStatusResponse {}

View File

@ -0,0 +1,20 @@
syntax = "proto3";
package zgc.pricefeed.v1beta1;
import "gogoproto/gogo.proto";
import "zgc/pricefeed/v1beta1/store.proto";
option go_package = "github.com/0glabs/0g-chain/x/pricefeed/types";
option (gogoproto.equal_all) = true;
option (gogoproto.verbose_equal_all) = true;
// GenesisState defines the pricefeed module's genesis state.
message GenesisState {
// params defines all the parameters of the module.
Params params = 1 [(gogoproto.nullable) = false];
repeated PostedPrice posted_prices = 2 [
(gogoproto.castrepeated) = "PostedPrices",
(gogoproto.nullable) = false
];
}

View File

@ -0,0 +1,163 @@
syntax = "proto3";
package zgc.pricefeed.v1beta1;
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "zgc/pricefeed/v1beta1/store.proto";
option go_package = "github.com/0glabs/0g-chain/x/pricefeed/types";
option (gogoproto.equal_all) = true;
option (gogoproto.verbose_equal_all) = true;
// Query defines the gRPC querier service for pricefeed module
service Query {
// Params queries all parameters of the pricefeed module.
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/0g-chain/pricefeed/v1beta1/params";
}
// Price queries price details based on a market
rpc Price(QueryPriceRequest) returns (QueryPriceResponse) {
option (google.api.http).get = "/0g-chain/pricefeed/v1beta1/prices/{market_id}";
}
// Prices queries all prices
rpc Prices(QueryPricesRequest) returns (QueryPricesResponse) {
option (google.api.http).get = "/0g-chain/pricefeed/v1beta1/prices";
}
// RawPrices queries all raw prices based on a market
rpc RawPrices(QueryRawPricesRequest) returns (QueryRawPricesResponse) {
option (google.api.http).get = "/0g-chain/pricefeed/v1beta1/rawprices/{market_id}";
}
// Oracles queries all oracles based on a market
rpc Oracles(QueryOraclesRequest) returns (QueryOraclesResponse) {
option (google.api.http).get = "/0g-chain/pricefeed/v1beta1/oracles/{market_id}";
}
// Markets queries all markets
rpc Markets(QueryMarketsRequest) returns (QueryMarketsResponse) {
option (google.api.http).get = "/0g-chain/pricefeed/v1beta1/markets";
}
}
// QueryParamsRequest defines the request type for querying x/pricefeed
// parameters.
message QueryParamsRequest {}
// QueryParamsResponse defines the response type for querying x/pricefeed
// parameters.
message QueryParamsResponse {
option (gogoproto.goproto_getters) = false;
Params params = 1 [(gogoproto.nullable) = false];
}
// QueryPriceRequest is the request type for the Query/PriceRequest RPC method.
message QueryPriceRequest {
option (gogoproto.goproto_getters) = false;
string market_id = 1;
}
// QueryPriceResponse is the response type for the Query/Prices RPC method.
message QueryPriceResponse {
option (gogoproto.goproto_getters) = false;
CurrentPriceResponse price = 1 [(gogoproto.nullable) = false];
}
// QueryPricesRequest is the request type for the Query/Prices RPC method.
message QueryPricesRequest {}
// QueryPricesResponse is the response type for the Query/Prices RPC method.
message QueryPricesResponse {
option (gogoproto.goproto_getters) = false;
repeated CurrentPriceResponse prices = 1 [
(gogoproto.castrepeated) = "CurrentPriceResponses",
(gogoproto.nullable) = false
];
}
// QueryRawPricesRequest is the request type for the Query/RawPrices RPC method.
message QueryRawPricesRequest {
option (gogoproto.goproto_getters) = false;
string market_id = 1;
}
// QueryRawPricesResponse is the response type for the Query/RawPrices RPC
// method.
message QueryRawPricesResponse {
option (gogoproto.goproto_getters) = false;
repeated PostedPriceResponse raw_prices = 1 [
(gogoproto.castrepeated) = "PostedPriceResponses",
(gogoproto.nullable) = false
];
}
// QueryOraclesRequest is the request type for the Query/Oracles RPC method.
message QueryOraclesRequest {
option (gogoproto.goproto_getters) = false;
string market_id = 1;
}
// QueryOraclesResponse is the response type for the Query/Oracles RPC method.
message QueryOraclesResponse {
option (gogoproto.goproto_getters) = false;
// List of oracle addresses
repeated string oracles = 1;
}
// QueryMarketsRequest is the request type for the Query/Markets RPC method.
message QueryMarketsRequest {}
// QueryMarketsResponse is the response type for the Query/Markets RPC method.
message QueryMarketsResponse {
option (gogoproto.goproto_getters) = false;
// List of markets
repeated MarketResponse markets = 1 [
(gogoproto.castrepeated) = "MarketResponses",
(gogoproto.nullable) = false
];
}
// PostedPriceResponse defines a price for market posted by a specific oracle.
message PostedPriceResponse {
string market_id = 1 [(gogoproto.customname) = "MarketID"];
string oracle_address = 2;
string price = 3 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
google.protobuf.Timestamp expiry = 4 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false
];
}
// CurrentPriceResponse defines a current price for a particular market in the pricefeed
// module.
message CurrentPriceResponse {
string market_id = 1 [(gogoproto.customname) = "MarketID"];
string price = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}
// MarketResponse defines an asset in the pricefeed.
message MarketResponse {
string market_id = 1 [(gogoproto.customname) = "MarketID"];
string base_asset = 2;
string quote_asset = 3;
repeated string oracles = 4;
bool active = 5;
}

View File

@ -0,0 +1,57 @@
syntax = "proto3";
package zgc.pricefeed.v1beta1;
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/0glabs/0g-chain/x/pricefeed/types";
option (gogoproto.equal_all) = true;
option (gogoproto.verbose_equal_all) = true;
// Params defines the parameters for the pricefeed module.
message Params {
repeated Market markets = 1 [
(gogoproto.castrepeated) = "Markets",
(gogoproto.nullable) = false
];
}
// Market defines an asset in the pricefeed.
message Market {
string market_id = 1 [(gogoproto.customname) = "MarketID"];
string base_asset = 2;
string quote_asset = 3;
repeated bytes oracles = 4 [
(cosmos_proto.scalar) = "cosmos.AddressBytes",
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
];
bool active = 5;
}
// PostedPrice defines a price for market posted by a specific oracle.
message PostedPrice {
string market_id = 1 [(gogoproto.customname) = "MarketID"];
bytes oracle_address = 2 [
(cosmos_proto.scalar) = "cosmos.AddressBytes",
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"
];
string price = 3 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
google.protobuf.Timestamp expiry = 4 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false
];
}
// CurrentPrice defines a current price for a particular market in the pricefeed
// module.
message CurrentPrice {
string market_id = 1 [(gogoproto.customname) = "MarketID"];
string price = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

View File

@ -0,0 +1,35 @@
syntax = "proto3";
package zgc.pricefeed.v1beta1;
import "gogoproto/gogo.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/0glabs/0g-chain/x/pricefeed/types";
option (gogoproto.equal_all) = true;
option (gogoproto.verbose_equal_all) = true;
// Msg defines the pricefeed Msg service.
service Msg {
// PostPrice defines a method for creating a new post price
rpc PostPrice(MsgPostPrice) returns (MsgPostPriceResponse);
}
// MsgPostPrice represents a method for creating a new post price
message MsgPostPrice {
option (gogoproto.goproto_getters) = false;
// address of client
string from = 1;
string market_id = 2 [(gogoproto.customname) = "MarketID"];
string price = 3 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
google.protobuf.Timestamp expiry = 4 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false
];
}
// MsgPostPriceResponse defines the Msg/PostPrice response type.
message MsgPostPriceResponse {}

20
x/bep3/abci.go Normal file
View File

@ -0,0 +1,20 @@
package bep3
import (
"time"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// BeginBlocker on every block expires outdated atomic swaps and removes closed
// swap from long term storage (default storage time of 1 week)
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker)
k.UpdateTimeBasedSupplyLimits(ctx)
k.UpdateExpiredAtomicSwaps(ctx)
k.DeleteClosedAtomicSwapsFromLongtermStorage(ctx)
}

243
x/bep3/abci_test.go Normal file
View File

@ -0,0 +1,243 @@
package bep3_test
import (
"testing"
"github.com/stretchr/testify/suite"
tmbytes "github.com/cometbft/cometbft/libs/bytes"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type ABCITestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
swapIDs []tmbytes.HexBytes
randomNumbers []tmbytes.HexBytes
}
func (suite *ABCITestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
// Set up auth GenesisState
_, addrs := app.GeneratePrivKeyAddressPairs(12)
coins := sdk.NewCoins(c("bnb", 10000000000), c("a0gi", 10000))
authGS := app.NewFundedGenStateWithSameCoins(tApp.AppCodec(), coins, addrs)
// Initialize test app
tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(tApp.AppCodec(), addrs[11]))
suite.ctx = ctx
suite.app = tApp
suite.addrs = addrs
suite.ResetKeeper()
}
func (suite *ABCITestSuite) ResetKeeper() {
suite.keeper = suite.app.GetBep3Keeper()
var swapIDs []tmbytes.HexBytes
var randomNumbers []tmbytes.HexBytes
for i := 0; i < 10; i++ {
// Set up atomic swap variables
expireHeight := types.DefaultMinBlockLock
amount := cs(c("bnb", int64(10000)))
timestamp := ts(i)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
// Create atomic swap and check err to confirm creation
err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight,
suite.addrs[11], suite.addrs[i], TestSenderOtherChain, TestRecipientOtherChain,
amount, true)
suite.Nil(err)
// Store swap's calculated ID and secret random number
swapID := types.CalculateSwapID(randomNumberHash, suite.addrs[11], TestSenderOtherChain)
swapIDs = append(swapIDs, swapID)
randomNumbers = append(randomNumbers, randomNumber[:])
}
suite.swapIDs = swapIDs
suite.randomNumbers = randomNumbers
}
func (suite *ABCITestSuite) TestBeginBlocker_UpdateExpiredAtomicSwaps() {
testCases := []struct {
name string
firstCtx sdk.Context
secondCtx sdk.Context
expectedStatus types.SwapStatus
expectInStorage bool
}{
{
name: "normal",
firstCtx: suite.ctx,
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 10),
expectedStatus: types.SWAP_STATUS_OPEN,
expectInStorage: true,
},
{
name: "after expiration",
firstCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 410),
expectedStatus: types.SWAP_STATUS_EXPIRED,
expectInStorage: true,
},
{
name: "after completion",
firstCtx: suite.ctx,
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 10),
expectedStatus: types.SWAP_STATUS_COMPLETED,
expectInStorage: true,
},
{
name: "after deletion",
firstCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400 + int64(types.DefaultLongtermStorageDuration)),
expectedStatus: types.SWAP_STATUS_UNSPECIFIED,
expectInStorage: false,
},
}
for _, tc := range testCases {
// Reset keeper and run the initial begin blocker
suite.ResetKeeper()
suite.Run(tc.name, func() {
bep3.BeginBlocker(tc.firstCtx, suite.keeper)
switch tc.expectedStatus {
case types.SWAP_STATUS_COMPLETED:
for i, swapID := range suite.swapIDs {
err := suite.keeper.ClaimAtomicSwap(tc.firstCtx, suite.addrs[5], swapID, suite.randomNumbers[i])
suite.Nil(err)
}
case types.SWAP_STATUS_UNSPECIFIED:
for _, swapID := range suite.swapIDs {
err := suite.keeper.RefundAtomicSwap(tc.firstCtx, suite.addrs[5], swapID)
suite.Nil(err)
}
}
// Run the second begin blocker
bep3.BeginBlocker(tc.secondCtx, suite.keeper)
// Check each swap's availibility and status
for _, swapID := range suite.swapIDs {
storedSwap, found := suite.keeper.GetAtomicSwap(tc.secondCtx, swapID)
if tc.expectInStorage {
suite.True(found)
} else {
suite.False(found)
}
suite.Equal(tc.expectedStatus, storedSwap.Status)
}
})
}
}
func (suite *ABCITestSuite) TestBeginBlocker_DeleteClosedAtomicSwapsFromLongtermStorage() {
type Action int
const (
NULL Action = 0x00
Refund Action = 0x01
Claim Action = 0x02
)
testCases := []struct {
name string
firstCtx sdk.Context
action Action
secondCtx sdk.Context
expectInStorage bool
}{
{
name: "no action with long storage duration",
firstCtx: suite.ctx,
action: NULL,
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + int64(types.DefaultLongtermStorageDuration)),
expectInStorage: true,
},
{
name: "claim with short storage duration",
firstCtx: suite.ctx,
action: Claim,
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 5000),
expectInStorage: true,
},
{
name: "claim with long storage duration",
firstCtx: suite.ctx,
action: Claim,
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + int64(types.DefaultLongtermStorageDuration)),
expectInStorage: false,
},
{
name: "refund with short storage duration",
firstCtx: suite.ctx,
action: Refund,
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 5000),
expectInStorage: true,
},
{
name: "refund with long storage duration",
firstCtx: suite.ctx,
action: Refund,
secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + int64(types.DefaultLongtermStorageDuration)),
expectInStorage: false,
},
}
for _, tc := range testCases {
// Reset keeper and run the initial begin blocker
suite.ResetKeeper()
suite.Run(tc.name, func() {
bep3.BeginBlocker(tc.firstCtx, suite.keeper)
switch tc.action {
case Claim:
for i, swapID := range suite.swapIDs {
err := suite.keeper.ClaimAtomicSwap(tc.firstCtx, suite.addrs[5], swapID, suite.randomNumbers[i])
suite.Nil(err)
}
case Refund:
for _, swapID := range suite.swapIDs {
swap, _ := suite.keeper.GetAtomicSwap(tc.firstCtx, swapID)
refundCtx := suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + int64(swap.ExpireHeight))
bep3.BeginBlocker(refundCtx, suite.keeper)
err := suite.keeper.RefundAtomicSwap(refundCtx, suite.addrs[5], swapID)
suite.Nil(err)
// Add expire height to second ctx block height
tc.secondCtx = tc.secondCtx.WithBlockHeight(tc.secondCtx.BlockHeight() + int64(swap.ExpireHeight))
}
}
// Run the second begin blocker
bep3.BeginBlocker(tc.secondCtx, suite.keeper)
// Check each swap's availability and status
for _, swapID := range suite.swapIDs {
_, found := suite.keeper.GetAtomicSwap(tc.secondCtx, swapID)
if tc.expectInStorage {
suite.True(found)
} else {
suite.False(found)
}
}
})
}
}
func TestABCITestSuite(t *testing.T) {
suite.Run(t, new(ABCITestSuite))
}

336
x/bep3/client/cli/query.go Normal file
View File

@ -0,0 +1,336 @@
package cli
import (
"context"
"encoding/hex"
"fmt"
"strconv"
"strings"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/cobra"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// Query atomic swaps flags
const (
flagInvolve = "involve"
flagExpiration = "expiration"
flagStatus = "status"
flagDirection = "direction"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string) *cobra.Command {
// Group bep3 queries under a subcommand
bep3QueryCmd := &cobra.Command{
Use: "bep3",
Short: "Querying commands for the bep3 module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
cmds := []*cobra.Command{
QueryCalcSwapIDCmd(queryRoute),
QueryCalcRandomNumberHashCmd(queryRoute),
QueryGetAssetSupplyCmd(queryRoute),
QueryGetAssetSuppliesCmd(queryRoute),
QueryGetAtomicSwapCmd(queryRoute),
QueryGetAtomicSwapsCmd(queryRoute),
QueryParamsCmd(queryRoute),
}
for _, cmd := range cmds {
flags.AddQueryFlagsToCmd(cmd)
}
bep3QueryCmd.AddCommand(cmds...)
return bep3QueryCmd
}
// QueryCalcRandomNumberHashCmd calculates the random number hash for a number and timestamp
func QueryCalcRandomNumberHashCmd(queryRoute string) *cobra.Command {
return &cobra.Command{
Use: "calc-rnh [unix-timestamp]",
Short: "calculates an example random number hash from an optional timestamp",
Example: "bep3 calc-rnh now",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
userTimestamp := "now"
if len(args) > 0 {
userTimestamp = args[0]
}
// Timestamp defaults to time.Now() unless it's explicitly set
var timestamp int64
if strings.Compare(userTimestamp, "now") == 0 {
timestamp = tmtime.Now().Unix()
} else {
userTimestamp, err := strconv.ParseInt(userTimestamp, 10, 64)
if err != nil {
return err
}
timestamp = userTimestamp
}
// Load hex-encoded cryptographically strong pseudo-random number
randomNumber, err := types.GenerateSecureRandomNumber()
if err != nil {
return err
}
randomNumberHash := types.CalculateRandomHash(randomNumber, timestamp)
// Prepare random number, timestamp, and hash for output
randomNumberStr := fmt.Sprintf("Random number: %s\n", hex.EncodeToString(randomNumber))
timestampStr := fmt.Sprintf("Timestamp: %d\n", timestamp)
randomNumberHashStr := fmt.Sprintf("Random number hash: %s", hex.EncodeToString(randomNumberHash))
output := []string{randomNumberStr, timestampStr, randomNumberHashStr}
return clientCtx.PrintObjectLegacy(strings.Join(output, ""))
},
}
}
// QueryCalcSwapIDCmd calculates the swapID for a random number hash, sender, and sender other chain
func QueryCalcSwapIDCmd(queryRoute string) *cobra.Command {
return &cobra.Command{
Use: "calc-swapid [random-number-hash] [sender] [sender-other-chain]",
Short: "calculate swap ID for the given random number hash, sender, and sender other chain",
Example: "bep3 calc-swapid 0677bd8a303dd981810f34d8e5cc6507f13b391899b84d3c1be6c6045a17d747 0g1l0xsq2z7gqd7yly0g40y5836g0appumark77ny bnb1ud3q90r98l3mhd87kswv3h8cgrymzeljct8qn7",
Args: cobra.MinimumNArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
// Parse query params
randomNumberHash, err := hex.DecodeString(args[0])
if err != nil {
return err
}
sender, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
senderOtherChain := args[2]
// Calculate swap ID and convert to human-readable string
swapID := types.CalculateSwapID(randomNumberHash, sender, senderOtherChain)
return clientCtx.PrintObjectLegacy(hex.EncodeToString(swapID))
},
}
}
// QueryGetAssetSupplyCmd queries as asset's current in swap supply, active, supply, and supply limit
func QueryGetAssetSupplyCmd(queryRoute string) *cobra.Command {
return &cobra.Command{
Use: "supply [denom]",
Short: "get information about an asset's supply",
Example: "bep3 supply bnb",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.AssetSupply(context.Background(), &types.QueryAssetSupplyRequest{
Denom: args[0],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// QueryGetAssetSuppliesCmd queries AssetSupplies in the store
func QueryGetAssetSuppliesCmd(queryRoute string) *cobra.Command {
return &cobra.Command{
Use: "supplies",
Short: "get a list of all asset supplies",
Example: "bep3 supplies",
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.AssetSupplies(context.Background(), &types.QueryAssetSuppliesRequest{
// TODO: Pagination here?
})
if err != nil {
return err
}
if len(res.AssetSupplies) == 0 {
return fmt.Errorf("there are currently no asset supplies")
}
return clientCtx.PrintProto(res)
},
}
}
// QueryGetAtomicSwapCmd queries an AtomicSwap by swapID
func QueryGetAtomicSwapCmd(queryRoute string) *cobra.Command {
return &cobra.Command{
Use: "swap [swap-id]",
Short: "get atomic swap information",
Example: "bep3 swap 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.AtomicSwap(context.Background(), &types.QueryAtomicSwapRequest{
SwapId: args[0],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// QueryGetAtomicSwapsCmd queries AtomicSwaps in the store
func QueryGetAtomicSwapsCmd(queryRoute string) *cobra.Command {
cmd := &cobra.Command{
Use: "swaps",
Short: "query atomic swaps with optional filters",
Long: strings.TrimSpace(`Query for all paginated atomic swaps that match optional filters:
Example:
$ kvcli q bep3 swaps --involve=0g1l0xsq2z7gqd7yly0g40y5836g0appumark77ny
$ kvcli q bep3 swaps --expiration=280
$ kvcli q bep3 swaps --status=(Open|Completed|Expired)
$ kvcli q bep3 swaps --direction=(Incoming|Outgoing)
$ kvcli q bep3 swaps --page=2 --limit=100
`,
),
RunE: func(cmd *cobra.Command, args []string) error {
bechInvolveAddr, err := cmd.Flags().GetString(flagInvolve)
if err != nil {
return err
}
strExpiration, err := cmd.Flags().GetString(flagExpiration)
if err != nil {
return err
}
strSwapStatus, err := cmd.Flags().GetString(flagStatus)
if err != nil {
return err
}
strSwapDirection, err := cmd.Flags().GetString(flagDirection)
if err != nil {
return err
}
pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}
req := types.QueryAtomicSwapsRequest{
Pagination: pageReq,
}
if len(bechInvolveAddr) != 0 {
involveAddr, err := sdk.AccAddressFromBech32(bechInvolveAddr)
if err != nil {
return err
}
req.Involve = involveAddr.String()
}
if len(strExpiration) != 0 {
expiration, err := strconv.ParseUint(strExpiration, 10, 64)
if err != nil {
return err
}
req.Expiration = expiration
}
if len(strSwapStatus) != 0 {
swapStatus := types.NewSwapStatusFromString(strSwapStatus)
if !swapStatus.IsValid() {
return fmt.Errorf("invalid swap status %s", strSwapStatus)
}
req.Status = swapStatus
}
if len(strSwapDirection) != 0 {
swapDirection := types.NewSwapDirectionFromString(strSwapDirection)
if !swapDirection.IsValid() {
return fmt.Errorf("invalid swap direction %s", strSwapDirection)
}
req.Direction = swapDirection
}
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.AtomicSwaps(context.Background(), &req)
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
cmd.Flags().String(flagInvolve, "", "(optional) filter by atomic swaps that involve an address")
cmd.Flags().String(flagExpiration, "", "(optional) filter by atomic swaps that expire before a block height")
cmd.Flags().String(flagStatus, "", "(optional) filter by atomic swap status, status: open/completed/expired")
cmd.Flags().String(flagDirection, "", "(optional) filter by atomic swap direction, direction: incoming/outgoing")
flags.AddPaginationFlagsToCmd(cmd, "swaps")
return cmd
}
// QueryParamsCmd queries the bep3 module parameters
func QueryParamsCmd(queryRoute string) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the bep3 module parameters",
Example: "bep3 params",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.Params(context.Background(), &types.QueryParamsRequest{})
if err != nil {
return err
}
return clientCtx.PrintProto(&res.Params)
},
}
}

195
x/bep3/client/cli/tx.go Normal file
View File

@ -0,0 +1,195 @@
package cli
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd() *cobra.Command {
bep3TxCmd := &cobra.Command{
Use: "bep3",
Short: "bep3 transactions subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
cmds := []*cobra.Command{
GetCmdCreateAtomicSwap(),
GetCmdClaimAtomicSwap(),
GetCmdRefundAtomicSwap(),
}
for _, cmd := range cmds {
flags.AddTxFlagsToCmd(cmd)
}
bep3TxCmd.AddCommand(cmds...)
return bep3TxCmd
}
// GetCmdCreateAtomicSwap cli command for creating atomic swaps
func GetCmdCreateAtomicSwap() *cobra.Command {
return &cobra.Command{
Use: "create [to] [recipient-other-chain] [sender-other-chain] [timestamp] [coins] [height-span]",
Short: "create a new atomic swap",
Example: fmt.Sprintf("%s tx %s create 0g1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7 bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7 now 100bnb 270 --from validator",
version.AppName, types.ModuleName),
Args: cobra.ExactArgs(6),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
from := clientCtx.GetFromAddress() // same as 0g-chain executor's deputy address
to, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
recipientOtherChain := args[1] // same as the other executor's deputy address
senderOtherChain := args[2]
// Timestamp defaults to time.Now() unless it's explicitly set
var timestamp int64
if strings.Compare(args[3], "now") == 0 {
timestamp = tmtime.Now().Unix()
} else {
timestamp, err = strconv.ParseInt(args[3], 10, 64)
if err != nil {
return err
}
}
// Generate cryptographically strong pseudo-random number
randomNumber, err := types.GenerateSecureRandomNumber()
if err != nil {
return err
}
randomNumberHash := types.CalculateRandomHash(randomNumber, timestamp)
// Print random number, timestamp, and hash to user's console
fmt.Printf("\nRandom number: %s\n", hex.EncodeToString(randomNumber))
fmt.Printf("Timestamp: %d\n", timestamp)
fmt.Printf("Random number hash: %s\n\n", hex.EncodeToString(randomNumberHash))
coins, err := sdk.ParseCoinsNormalized(args[4])
if err != nil {
return err
}
heightSpan, err := strconv.ParseUint(args[5], 10, 64)
if err != nil {
return err
}
msg := types.NewMsgCreateAtomicSwap(
from.String(), to.String(), recipientOtherChain, senderOtherChain,
randomNumberHash, timestamp, coins, heightSpan,
)
err = msg.ValidateBasic()
if err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
},
}
}
// GetCmdClaimAtomicSwap cli command for claiming an atomic swap
func GetCmdClaimAtomicSwap() *cobra.Command {
return &cobra.Command{
Use: "claim [swap-id] [random-number]",
Short: "claim coins in an atomic swap using the secret number",
Example: fmt.Sprintf(
"%s tx %s claim 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af 56f13e6a5cd397447f8b5f8c82fdb5bbf56127db75269f5cc14e50acd8ac9a4c --from accA",
version.AppName, types.ModuleName,
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
from := clientCtx.GetFromAddress()
swapID, err := hex.DecodeString(args[0])
if err != nil {
return err
}
if len(strings.TrimSpace(args[1])) == 0 {
return fmt.Errorf("random-number cannot be empty")
}
randomNumber, err := hex.DecodeString(args[1])
if err != nil {
return err
}
msg := types.NewMsgClaimAtomicSwap(from.String(), swapID, randomNumber)
err = msg.ValidateBasic()
if err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
},
}
}
// GetCmdRefundAtomicSwap cli command for claiming an atomic swap
func GetCmdRefundAtomicSwap() *cobra.Command {
return &cobra.Command{
Use: "refund [swap-id]",
Short: "refund the coins in an atomic swap",
Example: fmt.Sprintf(
"%s tx %s refund 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af --from accA",
version.AppName, types.ModuleName,
),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
from := clientCtx.GetFromAddress()
swapID, err := hex.DecodeString(args[0])
if err != nil {
return err
}
msg := types.NewMsgRefundAtomicSwap(from.String(), swapID)
err = msg.ValidateBasic()
if err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
},
}
}

141
x/bep3/genesis.go Normal file
View File

@ -0,0 +1,141 @@
package bep3
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, accountKeeper types.AccountKeeper, gs *types.GenesisState) {
// Check if the module account exists
moduleAcc := accountKeeper.GetModuleAccount(ctx, types.ModuleName)
if moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
hasBurnPermissions := false
hasMintPermissions := false
for _, perm := range moduleAcc.GetPermissions() {
if perm == authtypes.Burner {
hasBurnPermissions = true
}
if perm == authtypes.Minter {
hasMintPermissions = true
}
}
if !hasBurnPermissions {
panic(fmt.Sprintf("%s module account does not have burn permissions", types.ModuleName))
}
if !hasMintPermissions {
panic(fmt.Sprintf("%s module account does not have mint permissions", types.ModuleName))
}
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err))
}
keeper.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
keeper.SetParams(ctx, gs.Params)
for _, supply := range gs.Supplies {
keeper.SetAssetSupply(ctx, supply, supply.GetDenom())
}
var incomingSupplies sdk.Coins
var outgoingSupplies sdk.Coins
for _, swap := range gs.AtomicSwaps {
if swap.Validate() != nil {
panic(fmt.Sprintf("invalid swap %s", swap.GetSwapID()))
}
// Atomic swap assets must be both supported and active
err := keeper.ValidateLiveAsset(ctx, swap.Amount[0])
if err != nil {
panic(fmt.Sprintf("swap has invalid asset: %s", err))
}
keeper.SetAtomicSwap(ctx, swap)
// Add swap to block index or longterm storage based on swap.Status
// Increment incoming or outgoing supply based on swap.Direction
switch swap.Direction {
case types.SWAP_DIRECTION_INCOMING:
switch swap.Status {
case types.SWAP_STATUS_OPEN:
// This index expires unclaimed swaps
keeper.InsertIntoByBlockIndex(ctx, swap)
incomingSupplies = incomingSupplies.Add(swap.Amount...)
case types.SWAP_STATUS_EXPIRED:
incomingSupplies = incomingSupplies.Add(swap.Amount...)
case types.SWAP_STATUS_COMPLETED:
// This index stores swaps until deletion
keeper.InsertIntoLongtermStorage(ctx, swap)
default:
panic(fmt.Sprintf("swap %s has invalid status %s", swap.GetSwapID(), swap.Status.String()))
}
case types.SWAP_DIRECTION_OUTGOING:
switch swap.Status {
case types.SWAP_STATUS_OPEN:
keeper.InsertIntoByBlockIndex(ctx, swap)
outgoingSupplies = outgoingSupplies.Add(swap.Amount...)
case types.SWAP_STATUS_EXPIRED:
outgoingSupplies = outgoingSupplies.Add(swap.Amount...)
case types.SWAP_STATUS_COMPLETED:
keeper.InsertIntoLongtermStorage(ctx, swap)
default:
panic(fmt.Sprintf("swap %s has invalid status %s", swap.GetSwapID(), swap.Status.String()))
}
default:
panic(fmt.Sprintf("swap %s has invalid direction %s", swap.GetSwapID(), swap.Direction.String()))
}
}
// Asset's given incoming/outgoing supply much match the amount of coins in incoming/outgoing atomic swaps
supplies := keeper.GetAllAssetSupplies(ctx)
for _, supply := range supplies {
incomingSupply := incomingSupplies.AmountOf(supply.GetDenom())
if !supply.IncomingSupply.Amount.Equal(incomingSupply) {
panic(fmt.Sprintf("asset's incoming supply %s does not match amount %s in incoming atomic swaps",
supply.IncomingSupply, incomingSupply))
}
outgoingSupply := outgoingSupplies.AmountOf(supply.GetDenom())
if !supply.OutgoingSupply.Amount.Equal(outgoingSupply) {
panic(fmt.Sprintf("asset's outgoing supply %s does not match amount %s in outgoing atomic swaps",
supply.OutgoingSupply, outgoingSupply))
}
limit, err := keeper.GetSupplyLimit(ctx, supply.GetDenom())
if err != nil {
panic(fmt.Sprintf("asset's supply limit not found: %s", err))
}
if supply.CurrentSupply.Amount.GT(limit.Limit) {
panic(fmt.Sprintf("asset's current supply %s is over the supply limit %s", supply.CurrentSupply, limit.Limit))
}
if supply.IncomingSupply.Amount.GT(limit.Limit) {
panic(fmt.Sprintf("asset's incoming supply %s is over the supply limit %s", supply.IncomingSupply, limit.Limit))
}
if supply.IncomingSupply.Amount.Add(supply.CurrentSupply.Amount).GT(limit.Limit) {
panic(fmt.Sprintf("asset's incoming supply + current supply %s is over the supply limit %s", supply.IncomingSupply.Add(supply.CurrentSupply), limit.Limit))
}
if supply.OutgoingSupply.Amount.GT(limit.Limit) {
panic(fmt.Sprintf("asset's outgoing supply %s is over the supply limit %s", supply.OutgoingSupply, limit.Limit))
}
}
}
// ExportGenesis writes the current store values to a genesis file, which can be imported again with InitGenesis
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) (data types.GenesisState) {
params := k.GetParams(ctx)
swaps := k.GetAllAtomicSwaps(ctx)
supplies := k.GetAllAssetSupplies(ctx)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = types.DefaultPreviousBlockTime
}
return types.NewGenesisState(params, swaps, supplies, previousBlockTime)
}

391
x/bep3/genesis_test.go Normal file
View File

@ -0,0 +1,391 @@
package bep3_test
import (
"testing"
"cosmossdk.io/math"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/stretchr/testify/suite"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type GenesisTestSuite struct {
suite.Suite
app app.TestApp
ctx sdk.Context
keeper keeper.Keeper
addrs []sdk.AccAddress
}
func (suite *GenesisTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
tApp := app.NewTestApp()
suite.ctx = tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
suite.keeper = tApp.GetBep3Keeper()
suite.app = tApp
_, addrs := app.GeneratePrivKeyAddressPairs(3)
suite.addrs = addrs
}
func (suite *GenesisTestSuite) TestModulePermissionsCheck() {
cdc := suite.app.AppCodec()
testCases := []struct {
name string
permissions []string
expectedPanic string
}{
{"no permissions", []string{}, "bep3 module account does not have burn permissions"},
{"mint permissions", []string{authtypes.Minter}, "bep3 module account does not have burn permissions"},
{"burn permissions", []string{authtypes.Burner}, "bep3 module account does not have mint permissions"},
{"burn and mint permissions", []string{authtypes.Burner, authtypes.Minter}, ""},
{"mint and burn permissions", []string{authtypes.Minter, authtypes.Burner}, ""},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
authGenesis := authtypes.NewGenesisState(
authtypes.DefaultParams(),
authtypes.GenesisAccounts{authtypes.NewEmptyModuleAccount(types.ModuleName, tc.permissions...)},
)
bep3Genesis := types.DefaultGenesisState()
genState := app.GenesisState{
authtypes.ModuleName: cdc.MustMarshalJSON(authGenesis),
types.ModuleName: cdc.MustMarshalJSON(&bep3Genesis),
}
initApp := func() { suite.app.InitializeFromGenesisStates(genState) }
if tc.expectedPanic == "" {
suite.NotPanics(initApp)
} else {
suite.PanicsWithValue(tc.expectedPanic, initApp)
}
})
}
}
func (suite *GenesisTestSuite) TestGenesisState() {
type GenState func() app.GenesisState
cdc := suite.app.AppCodec()
testCases := []struct {
name string
genState GenState
expectPass bool
expectedErr interface{}
}{
{
name: "default",
genState: func() app.GenesisState {
return NewBep3GenStateMulti(cdc, suite.addrs[0])
},
expectPass: true,
},
{
name: "import atomic swaps and asset supplies",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
_, addrs := app.GeneratePrivKeyAddressPairs(2)
var swaps types.AtomicSwaps
var supplies types.AssetSupplies
for i := 0; i < 2; i++ {
swap, supply := loadSwapAndSupply(addrs[i], i)
swaps = append(swaps, swap)
supplies = append(supplies, supply)
}
gs.AtomicSwaps = swaps
gs.Supplies = supplies
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: true,
},
{
name: "0 deputy fees",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
gs.Params.AssetParams[0].FixedFee = sdk.ZeroInt()
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: true,
},
{
name: "incoming supply doesn't match amount in incoming atomic swaps",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0]) // incoming supply is zero
_, addrs := app.GeneratePrivKeyAddressPairs(1)
swap, _ := loadSwapAndSupply(addrs[0], 0)
gs.AtomicSwaps = types.AtomicSwaps{swap}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "asset's incoming supply 0bnb does not match amount 50000 in incoming atomic swaps",
},
{
name: "current supply above limit",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
bnbSupplyLimit := math.ZeroInt()
for _, ap := range gs.Params.AssetParams {
if ap.Denom == "bnb" {
bnbSupplyLimit = ap.SupplyLimit.Limit
}
}
gs.Supplies = types.AssetSupplies{
types.NewAssetSupply(
c("bnb", 0),
c("bnb", 0),
sdk.NewCoin("bnb", bnbSupplyLimit.Add(i(1))),
c("bnb", 0),
0,
),
}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "asset's current supply 350000000000001bnb is over the supply limit 350000000000000",
},
{
name: "incoming supply above limit",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
// Set up overlimit amount
bnbSupplyLimit := math.ZeroInt()
for _, ap := range gs.Params.AssetParams {
if ap.Denom == "bnb" {
bnbSupplyLimit = ap.SupplyLimit.Limit
}
}
overLimitAmount := bnbSupplyLimit.Add(i(1))
// Set up an atomic swap with amount equal to the currently asset supply
_, addrs := app.GeneratePrivKeyAddressPairs(2)
timestamp := ts(0)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
swap := types.NewAtomicSwap(cs(c("bnb", overLimitAmount.Int64())), randomNumberHash,
types.DefaultMinBlockLock, timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
TestRecipientOtherChain, 0, types.SWAP_STATUS_OPEN, true, types.SWAP_DIRECTION_INCOMING)
gs.AtomicSwaps = types.AtomicSwaps{swap}
// Set up asset supply with overlimit current supply
gs.Supplies = types.AssetSupplies{
types.NewAssetSupply(
c("bnb", overLimitAmount.Int64()),
c("bnb", 0),
c("bnb", 0),
c("bnb", 0),
0,
),
}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "asset's incoming supply 350000000000001bnb is over the supply limit 350000000000000",
},
{
name: "incoming supply + current supply above limit",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
// Set up overlimit amount
bnbSupplyLimit := math.ZeroInt()
for _, ap := range gs.Params.AssetParams {
if ap.Denom == "bnb" {
bnbSupplyLimit = ap.SupplyLimit.Limit
}
}
halfLimit := bnbSupplyLimit.Int64() / 2
overHalfLimit := halfLimit + 1
// Set up an atomic swap with amount equal to the currently asset supply
_, addrs := app.GeneratePrivKeyAddressPairs(2)
timestamp := ts(0)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
swap := types.NewAtomicSwap(cs(c("bnb", halfLimit)), randomNumberHash,
uint64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
TestRecipientOtherChain, 0, types.SWAP_STATUS_OPEN, true, types.SWAP_DIRECTION_INCOMING)
gs.AtomicSwaps = types.AtomicSwaps{swap}
// Set up asset supply with overlimit supply
gs.Supplies = types.AssetSupplies{
types.NewAssetSupply(
c("bnb", halfLimit),
c("bnb", 0),
c("bnb", overHalfLimit),
c("bnb", 0),
0,
),
}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "asset's incoming supply + current supply 350000000000001bnb is over the supply limit 350000000000000",
},
{
name: "outgoing supply above limit",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
// Set up overlimit amount
bnbSupplyLimit := math.ZeroInt()
for _, ap := range gs.Params.AssetParams {
if ap.Denom == "bnb" {
bnbSupplyLimit = ap.SupplyLimit.Limit
}
}
overLimitAmount := bnbSupplyLimit.Add(i(1))
// Set up an atomic swap with amount equal to the currently asset supply
_, addrs := app.GeneratePrivKeyAddressPairs(2)
timestamp := ts(0)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
swap := types.NewAtomicSwap(cs(c("bnb", overLimitAmount.Int64())), randomNumberHash,
types.DefaultMinBlockLock, timestamp, addrs[1], suite.addrs[0], TestSenderOtherChain,
TestRecipientOtherChain, 0, types.SWAP_STATUS_OPEN, true, types.SWAP_DIRECTION_OUTGOING)
gs.AtomicSwaps = types.AtomicSwaps{swap}
// Set up asset supply with overlimit outgoing supply
gs.Supplies = types.AssetSupplies{
types.NewAssetSupply(
c("bnb", 0),
c("bnb", overLimitAmount.Int64()),
c("bnb", 0),
c("bnb", 0),
0,
),
}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "asset's outgoing supply 350000000000001bnb is over the supply limit 350000000000000",
},
{
name: "asset supply denom is not a supported asset",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
gs.Supplies = types.AssetSupplies{
types.NewAssetSupply(
c("fake", 0),
c("fake", 0),
c("fake", 0),
c("fake", 0),
0,
),
}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "asset's supply limit not found: fake: asset not found",
},
{
name: "atomic swap asset type is unsupported",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
_, addrs := app.GeneratePrivKeyAddressPairs(2)
timestamp := ts(0)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
swap := types.NewAtomicSwap(cs(c("fake", 500000)), randomNumberHash,
uint64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
TestRecipientOtherChain, 0, types.SWAP_STATUS_OPEN, true, types.SWAP_DIRECTION_INCOMING)
gs.AtomicSwaps = types.AtomicSwaps{swap}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "swap has invalid asset: fake: asset not found",
},
{
name: "atomic swap status is invalid",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
_, addrs := app.GeneratePrivKeyAddressPairs(2)
timestamp := ts(0)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
swap := types.NewAtomicSwap(cs(c("bnb", 5000)), randomNumberHash,
uint64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain,
TestRecipientOtherChain, 0, types.SWAP_STATUS_UNSPECIFIED, true, types.SWAP_DIRECTION_INCOMING)
gs.AtomicSwaps = types.AtomicSwaps{swap}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "failed to validate bep3 genesis state: invalid swap status",
},
{
name: "minimum block lock cannot be > maximum block lock",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
gs.Params.AssetParams[0].MinBlockLock = 201
gs.Params.AssetParams[0].MaxBlockLock = 200
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "failed to validate bep3 genesis state: asset bnb has minimum block lock > maximum block lock 201 > 200",
},
{
name: "empty supported asset denom",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
gs.Params.AssetParams[0].Denom = ""
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "failed to validate bep3 genesis state: asset denom invalid: ",
},
{
name: "negative supported asset limit",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
gs.Params.AssetParams[0].SupplyLimit.Limit = i(-100)
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "failed to validate bep3 genesis state: asset bnb has invalid (negative) supply limit: -100",
},
{
name: "duplicate supported asset denom",
genState: func() app.GenesisState {
gs := baseGenState(suite.addrs[0])
gs.Params.AssetParams[1].Denom = "bnb"
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&gs)}
},
expectPass: false,
expectedErr: "failed to validate bep3 genesis state: asset bnb cannot have duplicate denom",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
gs := tc.genState()
if tc.expectPass {
suite.NotPanics(func() {
suite.app.InitializeFromGenesisStates(gs)
}, tc.name)
} else {
suite.PanicsWithValue(tc.expectedErr, func() {
suite.app.InitializeFromGenesisStates(gs)
}, tc.name)
}
})
}
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

114
x/bep3/integration_test.go Normal file
View File

@ -0,0 +1,114 @@
package bep3_test
import (
"time"
sdkmath "cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
const (
TestSenderOtherChain = "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7"
TestRecipientOtherChain = "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7"
TestDeputy = "0g1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj"
TestUser = "0g1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p"
)
var (
StandardSupplyLimit = i(100000000000)
DenomMap = map[int]string{0: "bnb", 1: "inc"}
)
func i(in int64) sdkmath.Int { return sdkmath.NewInt(in) }
func d(de int64) sdk.Dec { return sdk.NewDec(de) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() }
func NewBep3GenStateMulti(cdc codec.JSONCodec, deputy sdk.AccAddress) app.GenesisState {
bep3Genesis := baseGenState(deputy)
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&bep3Genesis)}
}
func baseGenState(deputy sdk.AccAddress) types.GenesisState {
bep3Genesis := types.GenesisState{
Params: types.Params{
AssetParams: types.AssetParams{
{
Denom: "bnb",
CoinID: 714,
SupplyLimit: types.SupplyLimit{
Limit: sdkmath.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputy,
FixedFee: sdkmath.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdkmath.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
{
Denom: "inc",
CoinID: 9999,
SupplyLimit: types.SupplyLimit{
Limit: sdkmath.NewInt(100000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputy,
FixedFee: sdkmath.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdkmath.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
},
},
Supplies: types.AssetSupplies{
types.NewAssetSupply(
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
time.Duration(0),
),
types.NewAssetSupply(
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
time.Duration(0),
),
},
PreviousBlockTime: types.DefaultPreviousBlockTime,
}
return bep3Genesis
}
func loadSwapAndSupply(addr sdk.AccAddress, index int) (types.AtomicSwap, types.AssetSupply) {
coin := c(DenomMap[index], 50000)
expireOffset := types.DefaultMinBlockLock // Default expire height + offet to match timestamp
timestamp := ts(index) // One minute apart
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
swap := types.NewAtomicSwap(cs(coin), randomNumberHash,
expireOffset, timestamp, addr, addr, TestSenderOtherChain,
TestRecipientOtherChain, 1, types.SWAP_STATUS_OPEN, true, types.SWAP_DIRECTION_INCOMING)
supply := types.NewAssetSupply(coin, c(coin.Denom, 0),
c(coin.Denom, 0), c(coin.Denom, 0), time.Duration(0))
return swap, supply
}

184
x/bep3/keeper/asset.go Normal file
View File

@ -0,0 +1,184 @@
package keeper
import (
"time"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// IncrementCurrentAssetSupply increments an asset's supply by the coin
func (k Keeper) IncrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
supply, found := k.GetAssetSupply(ctx, coin.Denom)
if !found {
return errorsmod.Wrap(types.ErrAssetNotSupported, coin.Denom)
}
limit, err := k.GetSupplyLimit(ctx, coin.Denom)
if err != nil {
return err
}
supplyLimit := sdk.NewCoin(coin.Denom, limit.Limit)
// Resulting current supply must be under asset's limit
if supplyLimit.IsLT(supply.CurrentSupply.Add(coin)) {
return errorsmod.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, supply.CurrentSupply, supplyLimit)
}
if limit.TimeLimited {
timeBasedSupplyLimit := sdk.NewCoin(coin.Denom, limit.TimeBasedLimit)
if timeBasedSupplyLimit.IsLT(supply.TimeLimitedCurrentSupply.Add(coin)) {
return errorsmod.Wrapf(types.ErrExceedsTimeBasedSupplyLimit, "increase %s, current time-based asset supply %s, limit %s", coin, supply.TimeLimitedCurrentSupply, timeBasedSupplyLimit)
}
supply.TimeLimitedCurrentSupply = supply.TimeLimitedCurrentSupply.Add(coin)
}
supply.CurrentSupply = supply.CurrentSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
}
// DecrementCurrentAssetSupply decrement an asset's supply by the coin
func (k Keeper) DecrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
supply, found := k.GetAssetSupply(ctx, coin.Denom)
if !found {
return errorsmod.Wrap(types.ErrAssetNotSupported, coin.Denom)
}
// Resulting current supply must be greater than or equal to 0
// Use sdkmath.Int instead of sdk.Coin to prevent panic if true
if supply.CurrentSupply.Amount.Sub(coin.Amount).IsNegative() {
return errorsmod.Wrapf(types.ErrInvalidCurrentSupply, "decrease %s, asset supply %s", coin, supply.CurrentSupply)
}
supply.CurrentSupply = supply.CurrentSupply.Sub(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
}
// IncrementIncomingAssetSupply increments an asset's incoming supply
func (k Keeper) IncrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
supply, found := k.GetAssetSupply(ctx, coin.Denom)
if !found {
return errorsmod.Wrap(types.ErrAssetNotSupported, coin.Denom)
}
// Result of (current + incoming + amount) must be under asset's limit
totalSupply := supply.CurrentSupply.Add(supply.IncomingSupply)
limit, err := k.GetSupplyLimit(ctx, coin.Denom)
if err != nil {
return err
}
supplyLimit := sdk.NewCoin(coin.Denom, limit.Limit)
if supplyLimit.IsLT(totalSupply.Add(coin)) {
return errorsmod.Wrapf(types.ErrExceedsSupplyLimit, "increase %s, asset supply %s, limit %s", coin, totalSupply, supplyLimit)
}
if limit.TimeLimited {
timeLimitedTotalSupply := supply.TimeLimitedCurrentSupply.Add(supply.IncomingSupply)
timeBasedSupplyLimit := sdk.NewCoin(coin.Denom, limit.TimeBasedLimit)
if timeBasedSupplyLimit.IsLT(timeLimitedTotalSupply.Add(coin)) {
return errorsmod.Wrapf(types.ErrExceedsTimeBasedSupplyLimit, "increase %s, time-based asset supply %s, limit %s", coin, supply.TimeLimitedCurrentSupply, timeBasedSupplyLimit)
}
}
supply.IncomingSupply = supply.IncomingSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
}
// DecrementIncomingAssetSupply decrements an asset's incoming supply
func (k Keeper) DecrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
supply, found := k.GetAssetSupply(ctx, coin.Denom)
if !found {
return errorsmod.Wrap(types.ErrAssetNotSupported, coin.Denom)
}
// Resulting incoming supply must be greater than or equal to 0
// Use sdkmath.Int instead of sdk.Coin to prevent panic if true
if supply.IncomingSupply.Amount.Sub(coin.Amount).IsNegative() {
return errorsmod.Wrapf(types.ErrInvalidIncomingSupply, "decrease %s, incoming supply %s", coin, supply.IncomingSupply)
}
supply.IncomingSupply = supply.IncomingSupply.Sub(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
}
// IncrementOutgoingAssetSupply increments an asset's outgoing supply
func (k Keeper) IncrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
supply, found := k.GetAssetSupply(ctx, coin.Denom)
if !found {
return errorsmod.Wrap(types.ErrAssetNotSupported, coin.Denom)
}
// Result of (outgoing + amount) must be less than current supply
if supply.CurrentSupply.IsLT(supply.OutgoingSupply.Add(coin)) {
return errorsmod.Wrapf(types.ErrExceedsAvailableSupply, "swap amount %s, available supply %s", coin,
supply.CurrentSupply.Amount.Sub(supply.OutgoingSupply.Amount))
}
supply.OutgoingSupply = supply.OutgoingSupply.Add(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
}
// DecrementOutgoingAssetSupply decrements an asset's outgoing supply
func (k Keeper) DecrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) error {
supply, found := k.GetAssetSupply(ctx, coin.Denom)
if !found {
return errorsmod.Wrap(types.ErrAssetNotSupported, coin.Denom)
}
// Resulting outgoing supply must be greater than or equal to 0
// Use sdkmath.Int instead of sdk.Coin to prevent panic if true
if supply.OutgoingSupply.Amount.Sub(coin.Amount).IsNegative() {
return errorsmod.Wrapf(types.ErrInvalidOutgoingSupply, "decrease %s, outgoing supply %s", coin, supply.OutgoingSupply)
}
supply.OutgoingSupply = supply.OutgoingSupply.Sub(coin)
k.SetAssetSupply(ctx, supply, coin.Denom)
return nil
}
// CreateNewAssetSupply creates a new AssetSupply in the store for the input denom
func (k Keeper) CreateNewAssetSupply(ctx sdk.Context, denom string) types.AssetSupply {
supply := types.NewAssetSupply(
sdk.NewCoin(denom, sdk.ZeroInt()), sdk.NewCoin(denom, sdk.ZeroInt()),
sdk.NewCoin(denom, sdk.ZeroInt()), sdk.NewCoin(denom, sdk.ZeroInt()), time.Duration(0))
k.SetAssetSupply(ctx, supply, denom)
return supply
}
// UpdateTimeBasedSupplyLimits updates the time based supply for each asset, resetting it if the current time window has elapsed.
func (k Keeper) UpdateTimeBasedSupplyLimits(ctx sdk.Context) {
assets, found := k.GetAssets(ctx)
if !found {
return
}
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
k.SetPreviousBlockTime(ctx, previousBlockTime)
}
timeElapsed := ctx.BlockTime().Sub(previousBlockTime)
for _, asset := range assets {
supply, found := k.GetAssetSupply(ctx, asset.Denom)
// if a new asset has been added by governance, create a new asset supply for it in the store
if !found {
supply = k.CreateNewAssetSupply(ctx, asset.Denom)
}
newTimeElapsed := supply.TimeElapsed + timeElapsed
if asset.SupplyLimit.TimeLimited && newTimeElapsed < asset.SupplyLimit.TimePeriod {
supply.TimeElapsed = newTimeElapsed
} else {
supply.TimeElapsed = time.Duration(0)
supply.TimeLimitedCurrentSupply = sdk.NewCoin(asset.Denom, sdk.ZeroInt())
}
k.SetAssetSupply(ctx, supply, asset.Denom)
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}

703
x/bep3/keeper/asset_test.go Normal file
View File

@ -0,0 +1,703 @@
package keeper_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type AssetTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
}
func (suite *AssetTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
// Initialize genesis state
deputy, _ := sdk.AccAddressFromBech32(TestDeputy)
tApp.InitializeFromGenesisStates(NewBep3GenStateMulti(tApp.AppCodec(), deputy))
keeper := tApp.GetBep3Keeper()
params := keeper.GetParams(ctx)
params.AssetParams[0].SupplyLimit.Limit = sdkmath.NewInt(50)
params.AssetParams[1].SupplyLimit.Limit = sdkmath.NewInt(100)
params.AssetParams[1].SupplyLimit.TimeBasedLimit = sdkmath.NewInt(15)
keeper.SetParams(ctx, params)
// Set asset supply with standard value for testing
supply := types.NewAssetSupply(c("bnb", 5), c("bnb", 5), c("bnb", 40), c("bnb", 0), time.Duration(0))
keeper.SetAssetSupply(ctx, supply, supply.IncomingSupply.Denom)
supply = types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0))
keeper.SetAssetSupply(ctx, supply, supply.IncomingSupply.Denom)
keeper.SetPreviousBlockTime(ctx, ctx.BlockTime())
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
}
func (suite *AssetTestSuite) TestIncrementCurrentAssetSupply() {
type args struct {
coin sdk.Coin
}
testCases := []struct {
name string
args args
expectPass bool
}{
{
"normal",
args{
coin: c("bnb", 5),
},
true,
},
{
"equal limit",
args{
coin: c("bnb", 10),
},
true,
},
{
"exceeds limit",
args{
coin: c("bnb", 11),
},
false,
},
{
"unsupported asset",
args{
coin: c("xyz", 5),
},
false,
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coin)
postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
if tc.expectPass {
suite.True(found)
suite.NoError(err)
suite.Equal(preSupply.CurrentSupply.Add(tc.args.coin), postSupply.CurrentSupply)
} else {
suite.Error(err)
suite.Equal(preSupply.CurrentSupply, postSupply.CurrentSupply)
}
})
}
}
func (suite *AssetTestSuite) TestIncrementTimeLimitedCurrentAssetSupply() {
type args struct {
coin sdk.Coin
expectedSupply types.AssetSupply
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"normal",
args{
coin: c("inc", 5),
expectedSupply: types.AssetSupply{
IncomingSupply: c("inc", 10),
OutgoingSupply: c("inc", 5),
CurrentSupply: c("inc", 10),
TimeLimitedCurrentSupply: c("inc", 5),
TimeElapsed: time.Duration(0),
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"over limit",
args{
coin: c("inc", 16),
expectedSupply: types.AssetSupply{},
},
errArgs{
expectPass: false,
contains: "asset supply over limit for current time period",
},
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coin)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
supply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
suite.Require().Equal(tc.args.expectedSupply, supply)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *AssetTestSuite) TestDecrementCurrentAssetSupply() {
type args struct {
coin sdk.Coin
}
testCases := []struct {
name string
args args
expectPass bool
}{
{
"normal",
args{
coin: c("bnb", 30),
},
true,
},
{
"equal current",
args{
coin: c("bnb", 40),
},
true,
},
{
"exceeds current",
args{
coin: c("bnb", 41),
},
false,
},
{
"unsupported asset",
args{
coin: c("xyz", 30),
},
false,
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
err := suite.keeper.DecrementCurrentAssetSupply(suite.ctx, tc.args.coin)
postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
if tc.expectPass {
suite.True(found)
suite.NoError(err)
suite.True(preSupply.CurrentSupply.Sub(tc.args.coin).IsEqual(postSupply.CurrentSupply))
} else {
suite.Error(err)
suite.Equal(preSupply.CurrentSupply, postSupply.CurrentSupply)
}
})
}
}
func (suite *AssetTestSuite) TestIncrementIncomingAssetSupply() {
type args struct {
coin sdk.Coin
}
testCases := []struct {
name string
args args
expectPass bool
}{
{
"normal",
args{
coin: c("bnb", 2),
},
true,
},
{
"incoming + current = limit",
args{
coin: c("bnb", 5),
},
true,
},
{
"incoming + current > limit",
args{
coin: c("bnb", 6),
},
false,
},
{
"unsupported asset",
args{
coin: c("xyz", 2),
},
false,
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
err := suite.keeper.IncrementIncomingAssetSupply(suite.ctx, tc.args.coin)
postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
if tc.expectPass {
suite.True(found)
suite.NoError(err)
suite.Equal(preSupply.IncomingSupply.Add(tc.args.coin), postSupply.IncomingSupply)
} else {
suite.Error(err)
suite.Equal(preSupply.IncomingSupply, postSupply.IncomingSupply)
}
})
}
}
func (suite *AssetTestSuite) TestIncrementTimeLimitedIncomingAssetSupply() {
type args struct {
coin sdk.Coin
expectedSupply types.AssetSupply
}
type errArgs struct {
expectPass bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"normal",
args{
coin: c("inc", 5),
expectedSupply: types.AssetSupply{
IncomingSupply: c("inc", 15),
OutgoingSupply: c("inc", 5),
CurrentSupply: c("inc", 5),
TimeLimitedCurrentSupply: c("inc", 0),
TimeElapsed: time.Duration(0),
},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"over limit",
args{
coin: c("inc", 6),
expectedSupply: types.AssetSupply{},
},
errArgs{
expectPass: false,
contains: "asset supply over limit for current time period",
},
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
err := suite.keeper.IncrementIncomingAssetSupply(suite.ctx, tc.args.coin)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
supply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
suite.Require().Equal(tc.args.expectedSupply, supply)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *AssetTestSuite) TestDecrementIncomingAssetSupply() {
type args struct {
coin sdk.Coin
}
testCases := []struct {
name string
args args
expectPass bool
}{
{
"normal",
args{
coin: c("bnb", 4),
},
true,
},
{
"equal incoming",
args{
coin: c("bnb", 5),
},
true,
},
{
"exceeds incoming",
args{
coin: c("bnb", 6),
},
false,
},
{
"unsupported asset",
args{
coin: c("xyz", 4),
},
false,
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
err := suite.keeper.DecrementIncomingAssetSupply(suite.ctx, tc.args.coin)
postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
if tc.expectPass {
suite.True(found)
suite.NoError(err)
suite.True(preSupply.IncomingSupply.Sub(tc.args.coin).IsEqual(postSupply.IncomingSupply))
} else {
suite.Error(err)
suite.Equal(preSupply.IncomingSupply, postSupply.IncomingSupply)
}
})
}
}
func (suite *AssetTestSuite) TestIncrementOutgoingAssetSupply() {
type args struct {
coin sdk.Coin
}
testCases := []struct {
name string
args args
expectPass bool
}{
{
"normal",
args{
coin: c("bnb", 30),
},
true,
},
{
"outgoing + amount = current",
args{
coin: c("bnb", 35),
},
true,
},
{
"outoing + amount > current",
args{
coin: c("bnb", 36),
},
false,
},
{
"unsupported asset",
args{
coin: c("xyz", 30),
},
false,
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
err := suite.keeper.IncrementOutgoingAssetSupply(suite.ctx, tc.args.coin)
postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
if tc.expectPass {
suite.True(found)
suite.NoError(err)
suite.Equal(preSupply.OutgoingSupply.Add(tc.args.coin), postSupply.OutgoingSupply)
} else {
suite.Error(err)
suite.Equal(preSupply.OutgoingSupply, postSupply.OutgoingSupply)
}
})
}
}
func (suite *AssetTestSuite) TestDecrementOutgoingAssetSupply() {
type args struct {
coin sdk.Coin
}
testCases := []struct {
name string
args args
expectPass bool
}{
{
"normal",
args{
coin: c("bnb", 4),
},
true,
},
{
"equal outgoing",
args{
coin: c("bnb", 5),
},
true,
},
{
"exceeds outgoing",
args{
coin: c("bnb", 6),
},
false,
},
{
"unsupported asset",
args{
coin: c("xyz", 4),
},
false,
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
err := suite.keeper.DecrementOutgoingAssetSupply(suite.ctx, tc.args.coin)
postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, tc.args.coin.Denom)
if tc.expectPass {
suite.True(found)
suite.NoError(err)
suite.True(preSupply.OutgoingSupply.Sub(tc.args.coin).IsEqual(postSupply.OutgoingSupply))
} else {
suite.Error(err)
suite.Equal(preSupply.OutgoingSupply, postSupply.OutgoingSupply)
}
})
}
}
func (suite *AssetTestSuite) TestUpdateTimeBasedSupplyLimits() {
type args struct {
asset string
duration time.Duration
expectedSupply types.AssetSupply
}
type errArgs struct {
expectPanic bool
contains string
}
testCases := []struct {
name string
args args
errArgs errArgs
}{
{
"rate-limited increment time",
args{
asset: "inc",
duration: time.Second,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Second),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited increment time half",
args{
asset: "inc",
duration: time.Minute * 30,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Minute*30),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited period change",
args{
asset: "inc",
duration: time.Hour + time.Second,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited period change exact",
args{
asset: "inc",
duration: time.Hour,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"rate-limited period change big",
args{
asset: "inc",
duration: time.Hour * 4,
expectedSupply: types.NewAssetSupply(c("inc", 10), c("inc", 5), c("inc", 5), c("inc", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"non rate-limited increment time",
args{
asset: "bnb",
duration: time.Second,
expectedSupply: types.NewAssetSupply(c("bnb", 5), c("bnb", 5), c("bnb", 40), c("bnb", 0), time.Duration(0)),
},
errArgs{
expectPanic: false,
contains: "",
},
},
{
"new asset increment time",
args{
asset: "lol",
duration: time.Second,
expectedSupply: types.NewAssetSupply(c("lol", 0), c("lol", 0), c("lol", 0), c("lol", 0), time.Second),
},
errArgs{
expectPanic: false,
contains: "",
},
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
deputy, _ := sdk.AccAddressFromBech32(TestDeputy)
newParams := types.Params{
AssetParams: types.AssetParams{
{
Denom: "bnb",
CoinID: 714,
SupplyLimit: types.SupplyLimit{
Limit: sdkmath.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputy,
FixedFee: sdkmath.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdkmath.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
{
Denom: "inc",
CoinID: 9999,
SupplyLimit: types.SupplyLimit{
Limit: sdkmath.NewInt(100),
TimeLimited: true,
TimeBasedLimit: sdkmath.NewInt(10),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: deputy,
FixedFee: sdkmath.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdkmath.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
{
Denom: "lol",
CoinID: 9999,
SupplyLimit: types.SupplyLimit{
Limit: sdkmath.NewInt(100),
TimeLimited: true,
TimeBasedLimit: sdkmath.NewInt(10),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: deputy,
FixedFee: sdkmath.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdkmath.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
},
}
suite.keeper.SetParams(suite.ctx, newParams)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(tc.args.duration))
suite.NotPanics(
func() {
suite.keeper.UpdateTimeBasedSupplyLimits(suite.ctx)
},
)
if !tc.errArgs.expectPanic {
supply, found := suite.keeper.GetAssetSupply(suite.ctx, tc.args.asset)
suite.Require().True(found)
suite.Require().Equal(tc.args.expectedSupply, supply)
}
})
}
}
func TestAssetTestSuite(t *testing.T) {
suite.Run(t, new(AssetTestSuite))
}

181
x/bep3/keeper/grpc_query.go Normal file
View File

@ -0,0 +1,181 @@
package keeper
import (
"context"
"encoding/hex"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type queryServer struct {
keeper Keeper
}
// NewQueryServerImpl creates a new server for handling gRPC queries.
func NewQueryServerImpl(k Keeper) types.QueryServer {
return &queryServer{keeper: k}
}
var _ types.QueryServer = queryServer{}
// Params queries module params
func (s queryServer) Params(ctx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "empty request")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
params := s.keeper.GetParams(sdkCtx)
return &types.QueryParamsResponse{Params: params}, nil
}
// AssetSupply queries info about an asset's supply
func (s queryServer) AssetSupply(ctx context.Context, req *types.QueryAssetSupplyRequest) (*types.QueryAssetSupplyResponse, error) {
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "empty request")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
assetSupply, ok := s.keeper.GetAssetSupply(sdkCtx, req.Denom)
if !ok {
return nil, status.Errorf(codes.NotFound, "denom not found")
}
return &types.QueryAssetSupplyResponse{AssetSupply: mapAssetSupplyToResponse(assetSupply)}, nil
}
// AssetSupplies queries a list of asset supplies
func (s queryServer) AssetSupplies(ctx context.Context, req *types.QueryAssetSuppliesRequest) (*types.QueryAssetSuppliesResponse, error) {
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "empty request")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
var queryResults []types.AssetSupplyResponse
s.keeper.IterateAssetSupplies(sdkCtx, func(assetSupply types.AssetSupply) bool {
queryResults = append(queryResults, mapAssetSupplyToResponse(assetSupply))
return false
})
return &types.QueryAssetSuppliesResponse{
AssetSupplies: queryResults,
}, nil
}
// AtomicSwap queries info about an atomic swap
func (s queryServer) AtomicSwap(ctx context.Context, req *types.QueryAtomicSwapRequest) (*types.QueryAtomicSwapResponse, error) {
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "empty request")
}
swapId, err := hex.DecodeString(req.SwapId)
if err != nil {
return nil, status.Errorf(codes.NotFound, "invalid atomic swap id")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
atomicSwap, ok := s.keeper.GetAtomicSwap(sdkCtx, swapId)
if !ok {
return nil, status.Errorf(codes.NotFound, "invalid atomic swap")
}
return &types.QueryAtomicSwapResponse{
AtomicSwap: mapAtomicSwapToResponse(atomicSwap),
}, nil
}
// AtomicSwaps queries a list of atomic swaps
func (s queryServer) AtomicSwaps(ctx context.Context, req *types.QueryAtomicSwapsRequest) (*types.QueryAtomicSwapsResponse, error) {
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "empty request")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
store := prefix.NewStore(sdkCtx.KVStore(s.keeper.key), types.AtomicSwapKeyPrefix)
var queryResults []types.AtomicSwapResponse
pageRes, err := query.FilteredPaginate(store, req.Pagination, func(_, value []byte, shouldAccumulate bool) (bool, error) {
var atomicSwap types.AtomicSwap
err := s.keeper.cdc.Unmarshal(value, &atomicSwap)
if err != nil {
return false, err
}
if len(req.Involve) > 0 {
if atomicSwap.Sender.String() != req.Involve && atomicSwap.Recipient.String() != req.Involve {
return false, nil
}
}
// match expiration block limit (if supplied)
if req.Expiration > 0 {
if atomicSwap.ExpireHeight > req.Expiration {
return false, nil
}
}
// match status (if supplied/valid)
if req.Status.IsValid() {
if atomicSwap.Status != req.Status {
return false, nil
}
}
// match direction (if supplied/valid)
if req.Direction.IsValid() {
if atomicSwap.Direction != req.Direction {
return false, nil
}
}
if shouldAccumulate {
queryResults = append(queryResults, mapAtomicSwapToResponse(atomicSwap))
}
return true, nil
})
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err)
}
return &types.QueryAtomicSwapsResponse{
AtomicSwaps: queryResults,
Pagination: pageRes,
}, nil
}
func mapAssetSupplyToResponse(assetSupply types.AssetSupply) types.AssetSupplyResponse {
return types.AssetSupplyResponse{
IncomingSupply: assetSupply.IncomingSupply,
OutgoingSupply: assetSupply.OutgoingSupply,
CurrentSupply: assetSupply.CurrentSupply,
TimeLimitedCurrentSupply: assetSupply.TimeLimitedCurrentSupply,
TimeElapsed: assetSupply.TimeElapsed,
}
}
func mapAtomicSwapToResponse(atomicSwap types.AtomicSwap) types.AtomicSwapResponse {
return types.AtomicSwapResponse{
Id: atomicSwap.GetSwapID().String(),
Amount: atomicSwap.Amount,
RandomNumberHash: atomicSwap.RandomNumberHash.String(),
ExpireHeight: atomicSwap.ExpireHeight,
Timestamp: atomicSwap.Timestamp,
Sender: atomicSwap.Sender.String(),
Recipient: atomicSwap.Recipient.String(),
SenderOtherChain: atomicSwap.SenderOtherChain,
RecipientOtherChain: atomicSwap.RecipientOtherChain,
ClosedBlock: atomicSwap.ClosedBlock,
Status: atomicSwap.Status,
CrossChain: atomicSwap.CrossChain,
Direction: atomicSwap.Direction,
}
}

View File

@ -0,0 +1,119 @@
package keeper_test
import (
"time"
sdkmath "cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cometbft/cometbft/crypto"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
const (
TestSenderOtherChain = "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7"
TestRecipientOtherChain = "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7"
TestDeputy = "0g1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj"
)
var (
DenomMap = map[int]string{0: "btc", 1: "eth", 2: "bnb", 3: "xrp", 4: "dai"}
TestUser1 = sdk.AccAddress(crypto.AddressHash([]byte("0gTestUser1")))
TestUser2 = sdk.AccAddress(crypto.AddressHash([]byte("0gTestUser2")))
)
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() }
func NewAuthGenStateFromAccs(cdc codec.JSONCodec, accounts ...authtypes.GenesisAccount) app.GenesisState {
authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), accounts)
return app.GenesisState{authtypes.ModuleName: cdc.MustMarshalJSON(authGenesis)}
}
func NewBep3GenStateMulti(cdc codec.JSONCodec, deputyAddress sdk.AccAddress) app.GenesisState {
bep3Genesis := types.GenesisState{
Params: types.Params{
AssetParams: types.AssetParams{
{
Denom: "bnb",
CoinID: 714,
SupplyLimit: types.SupplyLimit{
Limit: sdkmath.NewInt(350000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
},
Active: true,
DeputyAddress: deputyAddress,
FixedFee: sdkmath.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdkmath.NewInt(1000000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
{
Denom: "inc",
CoinID: 9999,
SupplyLimit: types.SupplyLimit{
Limit: sdkmath.NewInt(100000000000000),
TimeLimited: true,
TimeBasedLimit: sdkmath.NewInt(50000000000),
TimePeriod: time.Hour,
},
Active: false,
DeputyAddress: deputyAddress,
FixedFee: sdkmath.NewInt(1000),
MinSwapAmount: sdk.OneInt(),
MaxSwapAmount: sdkmath.NewInt(100000000000),
MinBlockLock: types.DefaultMinBlockLock,
MaxBlockLock: types.DefaultMaxBlockLock,
},
},
},
Supplies: types.AssetSupplies{
types.NewAssetSupply(
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
sdk.NewCoin("bnb", sdk.ZeroInt()),
time.Duration(0),
),
types.NewAssetSupply(
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
sdk.NewCoin("inc", sdk.ZeroInt()),
time.Duration(0),
),
},
PreviousBlockTime: types.DefaultPreviousBlockTime,
}
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(&bep3Genesis)}
}
func atomicSwaps(ctx sdk.Context, count int) types.AtomicSwaps {
var swaps types.AtomicSwaps
for i := 0; i < count; i++ {
swap := atomicSwap(ctx, i)
swaps = append(swaps, swap)
}
return swaps
}
func atomicSwap(ctx sdk.Context, index int) types.AtomicSwap {
expireOffset := uint64(200) // Default expire height + offet to match timestamp
timestamp := ts(index) // One minute apart
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
return types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash,
uint64(ctx.BlockHeight())+expireOffset, timestamp, TestUser1, TestUser2,
TestSenderOtherChain, TestRecipientOtherChain, 0, types.SWAP_STATUS_OPEN, true,
types.SWAP_DIRECTION_INCOMING)
}

251
x/bep3/keeper/keeper.go Normal file
View File

@ -0,0 +1,251 @@
package keeper
import (
"fmt"
"time"
"github.com/cometbft/cometbft/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// Keeper of the bep3 store
type Keeper struct {
key storetypes.StoreKey
cdc codec.Codec
paramSubspace paramtypes.Subspace
bankKeeper types.BankKeeper
accountKeeper types.AccountKeeper
Maccs map[string]bool
}
// NewKeeper creates a bep3 keeper
func NewKeeper(cdc codec.Codec, key storetypes.StoreKey, sk types.BankKeeper, ak types.AccountKeeper,
paramstore paramtypes.Subspace, maccs map[string]bool,
) Keeper {
if !paramstore.HasKeyTable() {
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
}
keeper := Keeper{
key: key,
cdc: cdc,
paramSubspace: paramstore,
bankKeeper: sk,
accountKeeper: ak,
Maccs: maccs,
}
return keeper
}
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}
// ------------------------------------------
// Atomic Swaps
// ------------------------------------------
// SetAtomicSwap puts the AtomicSwap into the store, and updates any indexes.
func (k Keeper) SetAtomicSwap(ctx sdk.Context, atomicSwap types.AtomicSwap) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
bz := k.cdc.MustMarshal(&atomicSwap)
store.Set(atomicSwap.GetSwapID(), bz)
}
// GetAtomicSwap gets an AtomicSwap from the store.
func (k Keeper) GetAtomicSwap(ctx sdk.Context, swapID []byte) (types.AtomicSwap, bool) {
var atomicSwap types.AtomicSwap
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
bz := store.Get(swapID)
if bz == nil {
return atomicSwap, false
}
k.cdc.MustUnmarshal(bz, &atomicSwap)
return atomicSwap, true
}
// RemoveAtomicSwap removes an AtomicSwap from the AtomicSwapKeyPrefix.
func (k Keeper) RemoveAtomicSwap(ctx sdk.Context, swapID []byte) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
store.Delete(swapID)
}
// IterateAtomicSwaps provides an iterator over all stored AtomicSwaps.
// For each AtomicSwap, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateAtomicSwaps(ctx sdk.Context, cb func(atomicSwap types.AtomicSwap) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var atomicSwap types.AtomicSwap
k.cdc.MustUnmarshal(iterator.Value(), &atomicSwap)
if cb(atomicSwap) {
break
}
}
}
// GetAllAtomicSwaps returns all AtomicSwaps from the store
func (k Keeper) GetAllAtomicSwaps(ctx sdk.Context) (atomicSwaps types.AtomicSwaps) {
k.IterateAtomicSwaps(ctx, func(atomicSwap types.AtomicSwap) bool {
atomicSwaps = append(atomicSwaps, atomicSwap)
return false
})
return
}
// ------------------------------------------
// Atomic Swap Block Index
// ------------------------------------------
// InsertIntoByBlockIndex adds a swap ID and expiration time into the byBlock index.
func (k Keeper) InsertIntoByBlockIndex(ctx sdk.Context, atomicSwap types.AtomicSwap) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix)
store.Set(types.GetAtomicSwapByHeightKey(atomicSwap.ExpireHeight, atomicSwap.GetSwapID()), atomicSwap.GetSwapID())
}
// RemoveFromByBlockIndex removes an AtomicSwap from the byBlock index.
func (k Keeper) RemoveFromByBlockIndex(ctx sdk.Context, atomicSwap types.AtomicSwap) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix)
store.Delete(types.GetAtomicSwapByHeightKey(atomicSwap.ExpireHeight, atomicSwap.GetSwapID()))
}
// IterateAtomicSwapsByBlock provides an iterator over AtomicSwaps ordered by AtomicSwap expiration block
// For each AtomicSwap cb will be called. If cb returns true the iterator will close and stop.
func (k Keeper) IterateAtomicSwapsByBlock(ctx sdk.Context, inclusiveCutoffTime uint64, cb func(swapID []byte) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix)
iterator := store.Iterator(
nil, // start at the very start of the prefix store
sdk.PrefixEndBytes(sdk.Uint64ToBigEndian(inclusiveCutoffTime)), // end of range
)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
id := iterator.Value()
if cb(id) {
break
}
}
}
// ------------------------------------------
// Atomic Swap Longterm Storage Index
// ------------------------------------------
// InsertIntoLongtermStorage adds a swap ID and deletion time into the longterm storage index.
// Completed swaps are stored for 1 week.
func (k Keeper) InsertIntoLongtermStorage(ctx sdk.Context, atomicSwap types.AtomicSwap) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix)
deletionHeight := uint64(atomicSwap.ClosedBlock) + types.DefaultLongtermStorageDuration
store.Set(types.GetAtomicSwapByHeightKey(deletionHeight, atomicSwap.GetSwapID()), atomicSwap.GetSwapID())
}
// RemoveFromLongtermStorage removes a swap from the into the longterm storage index
func (k Keeper) RemoveFromLongtermStorage(ctx sdk.Context, atomicSwap types.AtomicSwap) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix)
deletionHeight := uint64(atomicSwap.ClosedBlock) + types.DefaultLongtermStorageDuration
store.Delete(types.GetAtomicSwapByHeightKey(deletionHeight, atomicSwap.GetSwapID()))
}
// IterateAtomicSwapsLongtermStorage provides an iterator over AtomicSwaps ordered by deletion height.
// For each AtomicSwap cb will be called. If cb returns true the iterator will close and stop.
func (k Keeper) IterateAtomicSwapsLongtermStorage(ctx sdk.Context, inclusiveCutoffTime uint64,
cb func(swapID []byte) (stop bool),
) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix)
iterator := store.Iterator(
nil, // start at the very start of the prefix store
sdk.PrefixEndBytes(sdk.Uint64ToBigEndian(inclusiveCutoffTime)), // end of range
)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
id := iterator.Value()
if cb(id) {
break
}
}
}
// ------------------------------------------
// Asset Supplies
// ------------------------------------------
// GetAssetSupply gets an asset's current supply from the store.
func (k Keeper) GetAssetSupply(ctx sdk.Context, denom string) (types.AssetSupply, bool) {
var assetSupply types.AssetSupply
store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return types.AssetSupply{}, false
}
k.cdc.MustUnmarshal(bz, &assetSupply)
return assetSupply, true
}
// SetAssetSupply updates an asset's supply
func (k Keeper) SetAssetSupply(ctx sdk.Context, supply types.AssetSupply, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyPrefix)
store.Set([]byte(denom), k.cdc.MustMarshal(&supply))
}
// IterateAssetSupplies provides an iterator over all stored AssetSupplies.
func (k Keeper) IterateAssetSupplies(ctx sdk.Context, cb func(supply types.AssetSupply) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.AssetSupplyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var supply types.AssetSupply
k.cdc.MustUnmarshal(iterator.Value(), &supply)
if cb(supply) {
break
}
}
}
// GetAllAssetSupplies returns all asset supplies from the store
func (k Keeper) GetAllAssetSupplies(ctx sdk.Context) (supplies types.AssetSupplies) {
k.IterateAssetSupplies(ctx, func(supply types.AssetSupply) bool {
supplies = append(supplies, supply)
return false
})
return
}
// GetPreviousBlockTime get the blocktime for the previous block
func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
b := store.Get(types.PreviousBlockTimeKey)
if b == nil {
return time.Time{}, false
}
if err := blockTime.UnmarshalBinary(b); err != nil {
panic(err)
}
return blockTime, true
}
// SetPreviousBlockTime set the time of the previous block
func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
b, err := blockTime.MarshalBinary()
if err != nil {
panic(err)
}
store.Set(types.PreviousBlockTimeKey, b)
}

View File

@ -0,0 +1,340 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
const LongtermStorageDuration = 86400
type KeeperTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
}
func (suite *KeeperTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
suite.ResetChain()
}
func (suite *KeeperTestSuite) ResetChain() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
keeper := tApp.GetBep3Keeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
}
func (suite *KeeperTestSuite) TestGetSetAtomicSwap() {
suite.ResetChain()
// Set new atomic swap
atomicSwap := atomicSwap(suite.ctx, 1)
suite.keeper.SetAtomicSwap(suite.ctx, atomicSwap)
// Check atomic swap in store
s, found := suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
suite.True(found)
suite.Equal(atomicSwap, s)
// Check fake atomic swap not in store
fakeSwapID := types.CalculateSwapID(atomicSwap.RandomNumberHash, TestUser2, "otheraddress")
_, found = suite.keeper.GetAtomicSwap(suite.ctx, fakeSwapID)
suite.False(found)
}
func (suite *KeeperTestSuite) TestRemoveAtomicSwap() {
suite.ResetChain()
// Set new atomic swap
atomicSwap := atomicSwap(suite.ctx, 1)
suite.keeper.SetAtomicSwap(suite.ctx, atomicSwap)
// Check atomic swap in store
s, found := suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
suite.True(found)
suite.Equal(atomicSwap, s)
suite.keeper.RemoveAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
// Check atomic swap not in store
_, found = suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID())
suite.False(found)
}
func (suite *KeeperTestSuite) TestIterateAtomicSwaps() {
suite.ResetChain()
// Set atomic swaps
atomicSwaps := atomicSwaps(suite.ctx, 4)
for _, s := range atomicSwaps {
suite.keeper.SetAtomicSwap(suite.ctx, s)
}
// Read each atomic swap from the store
var readAtomicSwaps types.AtomicSwaps
suite.keeper.IterateAtomicSwaps(suite.ctx, func(a types.AtomicSwap) bool {
readAtomicSwaps = append(readAtomicSwaps, a)
return false
})
// Check expected values
suite.Equal(len(atomicSwaps), len(readAtomicSwaps))
}
func (suite *KeeperTestSuite) TestGetAllAtomicSwaps() {
suite.ResetChain()
// Set atomic swaps
atomicSwaps := atomicSwaps(suite.ctx, 4)
for _, s := range atomicSwaps {
suite.keeper.SetAtomicSwap(suite.ctx, s)
}
// Get and check atomic swaps
res := suite.keeper.GetAllAtomicSwaps(suite.ctx)
suite.Equal(4, len(res))
}
func (suite *KeeperTestSuite) TestInsertIntoByBlockIndex() {
suite.ResetChain()
// Set new atomic swap in by block index
atomicSwap := atomicSwap(suite.ctx, 1)
suite.keeper.InsertIntoByBlockIndex(suite.ctx, atomicSwap)
// Block index lacks getter methods, must use iteration to get count of swaps in store
var swapIDs [][]byte
suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, atomicSwap.ExpireHeight+1, func(id []byte) bool {
swapIDs = append(swapIDs, id)
return false
})
suite.Equal(len(swapIDs), 1)
// Marshal the expected swapID
cdc := suite.app.LegacyAmino()
res, _ := cdc.Amino.MarshalBinaryBare(atomicSwap.GetSwapID())
expectedSwapID := res[1:]
suite.Equal(expectedSwapID, swapIDs[0])
}
func (suite *KeeperTestSuite) TestRemoveFromByBlockIndex() {
suite.ResetChain()
// Set new atomic swap in by block index
atomicSwap := atomicSwap(suite.ctx, 1)
suite.keeper.InsertIntoByBlockIndex(suite.ctx, atomicSwap)
// Check stored data in block index
var swapIDsPre [][]byte
suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, atomicSwap.ExpireHeight+1, func(id []byte) bool {
swapIDsPre = append(swapIDsPre, id)
return false
})
suite.Equal(len(swapIDsPre), 1)
suite.keeper.RemoveFromByBlockIndex(suite.ctx, atomicSwap)
// Check stored data not in block index
var swapIDsPost [][]byte
suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, atomicSwap.ExpireHeight+1, func(id []byte) bool {
swapIDsPost = append(swapIDsPost, id)
return false
})
suite.Equal(len(swapIDsPost), 0)
}
func (suite *KeeperTestSuite) TestIterateAtomicSwapsByBlock() {
suite.ResetChain()
type args struct {
blockCtx sdk.Context
swap types.AtomicSwap
}
var testCases []args
for i := 0; i < 8; i++ {
// Set up context 100 blocks apart
blockCtx := suite.ctx.WithBlockHeight(int64(i) * 100)
// Initialize a new atomic swap (different randomNumberHash = different swap IDs)
timestamp := tmtime.Now().Add(time.Duration(i) * time.Minute).Unix()
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
atomicSwap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash,
uint64(blockCtx.BlockHeight()), timestamp, TestUser1, TestUser2,
TestSenderOtherChain, TestRecipientOtherChain, 0, types.SWAP_STATUS_OPEN,
true, types.SWAP_DIRECTION_INCOMING)
// Insert into block index
suite.keeper.InsertIntoByBlockIndex(blockCtx, atomicSwap)
// Add to local block index
testCases = append(testCases, args{blockCtx, atomicSwap})
}
// Set up the expected swap IDs for a given cutoff block
cutoffBlock := int64(450)
var expectedSwapIDs [][]byte
for _, tc := range testCases {
if tc.blockCtx.BlockHeight() < cutoffBlock || tc.blockCtx.BlockHeight() == cutoffBlock {
expectedSwapIDs = append(expectedSwapIDs, tc.swap.GetSwapID())
}
}
// Read the swap IDs from store for a given cutoff block
var readSwapIDs [][]byte
suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(cutoffBlock), func(id []byte) bool {
readSwapIDs = append(readSwapIDs, id)
return false
})
suite.Equal(expectedSwapIDs, readSwapIDs)
}
func (suite *KeeperTestSuite) TestInsertIntoLongtermStorage() {
suite.ResetChain()
// Set atomic swap in longterm storage
atomicSwap := atomicSwap(suite.ctx, 1)
atomicSwap.ClosedBlock = suite.ctx.BlockHeight()
suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap)
// Longterm storage lacks getter methods, must use iteration to get count of swaps in store
var swapIDs [][]byte
suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool {
swapIDs = append(swapIDs, id)
return false
})
suite.Equal(len(swapIDs), 1)
// Marshal the expected swapID
cdc := suite.app.LegacyAmino()
res, _ := cdc.Amino.MarshalBinaryBare(atomicSwap.GetSwapID())
expectedSwapID := res[1:]
suite.Equal(expectedSwapID, swapIDs[0])
}
func (suite *KeeperTestSuite) TestRemoveFromLongtermStorage() {
suite.ResetChain()
// Set atomic swap in longterm storage
atomicSwap := atomicSwap(suite.ctx, 1)
atomicSwap.ClosedBlock = suite.ctx.BlockHeight()
suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap)
// Longterm storage lacks getter methods, must use iteration to get count of swaps in store
var swapIDs [][]byte
suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool {
swapIDs = append(swapIDs, id)
return false
})
suite.Equal(len(swapIDs), 1)
suite.keeper.RemoveFromLongtermStorage(suite.ctx, atomicSwap)
// Check stored data not in block index
var swapIDsPost [][]byte
suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool {
swapIDsPost = append(swapIDsPost, id)
return false
})
suite.Equal(len(swapIDsPost), 0)
}
func (suite *KeeperTestSuite) TestIterateAtomicSwapsLongtermStorage() {
suite.ResetChain()
// Set up atomic swaps with stagged closed blocks
var swaps types.AtomicSwaps
for i := 0; i < 8; i++ {
timestamp := tmtime.Now().Unix()
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
atomicSwap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash,
uint64(suite.ctx.BlockHeight()), timestamp, TestUser1, TestUser2,
TestSenderOtherChain, TestRecipientOtherChain, 100, types.SWAP_STATUS_OPEN,
true, types.SWAP_DIRECTION_INCOMING)
// Set closed block staggered by 100 blocks and insert into longterm storage
atomicSwap.ClosedBlock = int64(i) * 100
suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap)
// Add to local longterm storage
swaps = append(swaps, atomicSwap)
}
// Set up the expected swap IDs for a given cutoff block.
cutoffBlock := int64(LongtermStorageDuration + 350)
var expectedSwapIDs [][]byte
for _, swap := range swaps {
if swap.ClosedBlock+LongtermStorageDuration < cutoffBlock ||
swap.ClosedBlock+LongtermStorageDuration == cutoffBlock {
expectedSwapIDs = append(expectedSwapIDs, swap.GetSwapID())
}
}
// Read the swap IDs from store for a given cutoff block
var readSwapIDs [][]byte
suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(cutoffBlock), func(id []byte) bool {
readSwapIDs = append(readSwapIDs, id)
return false
})
// At the cutoff block, iteration should return half of the swap IDs
suite.Equal(len(swaps)/2, len(expectedSwapIDs))
suite.Equal(len(swaps)/2, len(readSwapIDs))
// Should be the same IDs
suite.Equal(expectedSwapIDs, readSwapIDs)
}
func (suite *KeeperTestSuite) TestGetSetAssetSupply() {
denom := "bnb"
// Put asset supply in store
assetSupply := types.NewAssetSupply(c(denom, 0), c(denom, 0), c(denom, 50000), c(denom, 0), time.Duration(0))
suite.keeper.SetAssetSupply(suite.ctx, assetSupply, denom)
// Check asset in store
storedAssetSupply, found := suite.keeper.GetAssetSupply(suite.ctx, denom)
suite.True(found)
suite.Equal(assetSupply, storedAssetSupply)
// Check fake asset supply not in store
fakeDenom := "xyz"
_, found = suite.keeper.GetAssetSupply(suite.ctx, fakeDenom)
suite.False(found)
}
func (suite *KeeperTestSuite) TestGetAllAssetSupplies() {
// Put asset supply in store
assetSupply := types.NewAssetSupply(c("bnb", 0), c("bnb", 0), c("bnb", 50000), c("bnb", 0), time.Duration(0))
suite.keeper.SetAssetSupply(suite.ctx, assetSupply, "bnb")
assetSupply = types.NewAssetSupply(c("inc", 0), c("inc", 0), c("inc", 50000), c("inc", 0), time.Duration(0))
suite.keeper.SetAssetSupply(suite.ctx, assetSupply, "inc")
supplies := suite.keeper.GetAllAssetSupplies(suite.ctx)
suite.Equal(2, len(supplies))
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

117
x/bep3/keeper/msg_server.go Normal file
View File

@ -0,0 +1,117 @@
package keeper
import (
"context"
"encoding/hex"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type msgServer struct {
keeper Keeper
}
// NewMsgServerImpl returns an implementation of the bep3 MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
return &msgServer{keeper: keeper}
}
var _ types.MsgServer = msgServer{}
func (k msgServer) CreateAtomicSwap(goCtx context.Context, msg *types.MsgCreateAtomicSwap) (*types.MsgCreateAtomicSwapResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return nil, err
}
to, err := sdk.AccAddressFromBech32(msg.To)
if err != nil {
return nil, err
}
randomNumberHash, err := hex.DecodeString(msg.RandomNumberHash)
if err != nil {
return nil, err
}
if err = k.keeper.CreateAtomicSwap(ctx, randomNumberHash, msg.Timestamp, msg.HeightSpan,
from, to, msg.SenderOtherChain, msg.RecipientOtherChain, msg.Amount, true); err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.From),
),
)
return &types.MsgCreateAtomicSwapResponse{}, nil
}
func (k msgServer) ClaimAtomicSwap(goCtx context.Context, msg *types.MsgClaimAtomicSwap) (*types.MsgClaimAtomicSwapResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return nil, err
}
swapID, err := hex.DecodeString(msg.SwapID)
if err != nil {
return nil, err
}
randomNumber, err := hex.DecodeString(msg.RandomNumber)
if err != nil {
return nil, err
}
err = k.keeper.ClaimAtomicSwap(ctx, from, swapID, randomNumber)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.From),
),
)
return &types.MsgClaimAtomicSwapResponse{}, nil
}
func (k msgServer) RefundAtomicSwap(goCtx context.Context, msg *types.MsgRefundAtomicSwap) (*types.MsgRefundAtomicSwapResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return nil, err
}
swapID, err := hex.DecodeString(msg.SwapID)
if err != nil {
return nil, err
}
err = k.keeper.RefundAtomicSwap(ctx, from, swapID)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.From),
),
)
return &types.MsgRefundAtomicSwapResponse{}, nil
}

View File

@ -0,0 +1,129 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
tmbytes "github.com/cometbft/cometbft/libs/bytes"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type MsgServerTestSuite struct {
suite.Suite
ctx sdk.Context
app app.TestApp
msgServer types.MsgServer
keeper keeper.Keeper
addrs []sdk.AccAddress
}
func (suite *MsgServerTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
cdc := tApp.AppCodec()
// Set up genesis state and initialize
_, addrs := app.GeneratePrivKeyAddressPairs(3)
coins := sdk.NewCoins(c("bnb", 10000000000), c("a0gi", 10000))
authGS := app.NewFundedGenStateWithSameCoins(tApp.AppCodec(), coins, addrs)
tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(cdc, addrs[0]))
suite.addrs = addrs
suite.keeper = tApp.GetBep3Keeper()
suite.msgServer = keeper.NewMsgServerImpl(suite.keeper)
suite.app = tApp
suite.ctx = ctx
}
func (suite *MsgServerTestSuite) AddAtomicSwap() (tmbytes.HexBytes, tmbytes.HexBytes) {
expireHeight := types.DefaultMinBlockLock
amount := cs(c("bnb", int64(50000)))
timestamp := ts(0)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
// Create atomic swap and check err to confirm creation
err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight,
suite.addrs[0], suite.addrs[1], TestSenderOtherChain, TestRecipientOtherChain,
amount, true)
suite.Nil(err)
swapID := types.CalculateSwapID(randomNumberHash, suite.addrs[0], TestSenderOtherChain)
return swapID, randomNumber[:]
}
func (suite *MsgServerTestSuite) TestMsgCreateAtomicSwap() {
amount := cs(c("bnb", int64(10000)))
timestamp := ts(0)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
msg := types.NewMsgCreateAtomicSwap(
suite.addrs[0].String(), suite.addrs[2].String(), TestRecipientOtherChain,
TestSenderOtherChain, randomNumberHash, timestamp, amount,
types.DefaultMinBlockLock)
res, err := suite.msgServer.CreateAtomicSwap(sdk.WrapSDKContext(suite.ctx), &msg)
suite.Require().NoError(err)
suite.Require().NotNil(res)
}
func (suite *MsgServerTestSuite) TestMsgClaimAtomicSwap() {
// Attempt claim msg on fake atomic swap
badRandomNumber, _ := types.GenerateSecureRandomNumber()
badRandomNumberHash := types.CalculateRandomHash(badRandomNumber[:], ts(0))
badSwapID := types.CalculateSwapID(badRandomNumberHash, suite.addrs[0], TestSenderOtherChain)
badMsg := types.NewMsgClaimAtomicSwap(suite.addrs[0].String(), badSwapID, badRandomNumber[:])
badRes, err := suite.msgServer.ClaimAtomicSwap(sdk.WrapSDKContext(suite.ctx), &badMsg)
suite.Require().Error(err)
suite.Require().Nil(badRes)
// Add an atomic swap before attempting new claim msg
swapID, randomNumber := suite.AddAtomicSwap()
msg := types.NewMsgClaimAtomicSwap(suite.addrs[0].String(), swapID, randomNumber)
res, err := suite.msgServer.ClaimAtomicSwap(sdk.WrapSDKContext(suite.ctx), &msg)
suite.Require().NoError(err)
suite.Require().NotNil(res)
}
func (suite *MsgServerTestSuite) TestMsgRefundAtomicSwap() {
// Attempt refund msg on fake atomic swap
badRandomNumber, _ := types.GenerateSecureRandomNumber()
badRandomNumberHash := types.CalculateRandomHash(badRandomNumber[:], ts(0))
badSwapID := types.CalculateSwapID(badRandomNumberHash, suite.addrs[0], TestSenderOtherChain)
badMsg := types.NewMsgRefundAtomicSwap(suite.addrs[0].String(), badSwapID)
badRes, err := suite.msgServer.RefundAtomicSwap(sdk.WrapSDKContext(suite.ctx), &badMsg)
suite.Require().Error(err)
suite.Require().Nil(badRes)
// Add an atomic swap and build refund msg
swapID, _ := suite.AddAtomicSwap()
msg := types.NewMsgRefundAtomicSwap(suite.addrs[0].String(), swapID)
// Attempt to refund active atomic swap
res1, err := suite.msgServer.RefundAtomicSwap(sdk.WrapSDKContext(suite.ctx), &msg)
suite.Require().Error(err)
suite.Require().Nil(res1)
// Expire the atomic swap with begin blocker and attempt refund
laterCtx := suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400)
bep3.BeginBlocker(laterCtx, suite.keeper)
res2, err := suite.msgServer.RefundAtomicSwap(sdk.WrapSDKContext(laterCtx), &msg)
suite.Require().NoError(err)
suite.Require().NotNil(res2)
}
func TestMsgServerTestSuite(t *testing.T) {
suite.Run(t, new(MsgServerTestSuite))
}

167
x/bep3/keeper/params.go Normal file
View File

@ -0,0 +1,167 @@
package keeper
import (
errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// GetParams returns the total set of bep3 parameters.
func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
k.paramSubspace.GetParamSet(ctx, &params)
return params
}
// SetParams sets the bep3 parameters to the param space.
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// ------------------------------------------
// Asset
// ------------------------------------------
// GetAsset returns the asset param associated with the input denom
func (k Keeper) GetAsset(ctx sdk.Context, denom string) (types.AssetParam, error) {
params := k.GetParams(ctx)
for _, asset := range params.AssetParams {
if denom == asset.Denom {
return asset, nil
}
}
return types.AssetParam{}, errorsmod.Wrap(types.ErrAssetNotSupported, denom)
}
// SetAsset sets an asset in the params
func (k Keeper) SetAsset(ctx sdk.Context, asset types.AssetParam) {
params := k.GetParams(ctx)
for i := range params.AssetParams {
if params.AssetParams[i].Denom == asset.Denom {
params.AssetParams[i] = asset
}
}
k.SetParams(ctx, params)
}
// GetAssets returns a list containing all supported assets
func (k Keeper) GetAssets(ctx sdk.Context) (types.AssetParams, bool) {
params := k.GetParams(ctx)
return params.AssetParams, len(params.AssetParams) > 0
}
// ------------------------------------------
// Asset-specific getters
// ------------------------------------------
// GetDeputyAddress returns the deputy address for the input denom
func (k Keeper) GetDeputyAddress(ctx sdk.Context, denom string) (sdk.AccAddress, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return sdk.AccAddress{}, err
}
return asset.DeputyAddress, nil
}
// GetFixedFee returns the fixed fee for incoming swaps
func (k Keeper) GetFixedFee(ctx sdk.Context, denom string) (sdkmath.Int, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return sdkmath.Int{}, err
}
return asset.FixedFee, nil
}
// GetMinSwapAmount returns the minimum swap amount
func (k Keeper) GetMinSwapAmount(ctx sdk.Context, denom string) (sdkmath.Int, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return sdkmath.Int{}, err
}
return asset.MinSwapAmount, nil
}
// GetMaxSwapAmount returns the maximum swap amount
func (k Keeper) GetMaxSwapAmount(ctx sdk.Context, denom string) (sdkmath.Int, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return sdkmath.Int{}, err
}
return asset.MaxSwapAmount, nil
}
// GetMinBlockLock returns the minimum block lock
func (k Keeper) GetMinBlockLock(ctx sdk.Context, denom string) (uint64, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return uint64(0), err
}
return asset.MinBlockLock, nil
}
// GetMaxBlockLock returns the maximum block lock
func (k Keeper) GetMaxBlockLock(ctx sdk.Context, denom string) (uint64, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return uint64(0), err
}
return asset.MaxBlockLock, nil
}
// GetAssetByCoinID returns an asset by its denom
func (k Keeper) GetAssetByCoinID(ctx sdk.Context, coinID int64) (types.AssetParam, bool) {
params := k.GetParams(ctx)
for _, asset := range params.AssetParams {
if asset.CoinID == coinID {
return asset, true
}
}
return types.AssetParam{}, false
}
// ValidateLiveAsset checks if an asset is both supported and active
func (k Keeper) ValidateLiveAsset(ctx sdk.Context, coin sdk.Coin) error {
asset, err := k.GetAsset(ctx, coin.Denom)
if err != nil {
return err
}
if !asset.Active {
return errorsmod.Wrap(types.ErrAssetNotActive, asset.Denom)
}
return nil
}
// GetSupplyLimit returns the supply limit for the input denom
func (k Keeper) GetSupplyLimit(ctx sdk.Context, denom string) (types.SupplyLimit, error) {
asset, err := k.GetAsset(ctx, denom)
if err != nil {
return types.SupplyLimit{}, err
}
return asset.SupplyLimit, nil
}
// ------------------------------------------
// Cross Asset Getters
// ------------------------------------------
// GetAuthorizedAddresses returns a list of addresses that have special authorization within this module, eg all the deputies.
func (k Keeper) GetAuthorizedAddresses(ctx sdk.Context) []sdk.AccAddress {
assetParams, found := k.GetAssets(ctx)
if !found {
// no assets params is a valid genesis state
return nil
}
var addresses []sdk.AccAddress
uniqueAddresses := map[string]bool{}
for _, ap := range assetParams {
a := ap.DeputyAddress
// de-dup addresses
if _, found := uniqueAddresses[a.String()]; !found {
addresses = append(addresses, a)
}
uniqueAddresses[a.String()] = true
}
return addresses
}

View File

@ -0,0 +1,178 @@
package keeper_test
import (
"errors"
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type ParamsTestSuite struct {
suite.Suite
keeper keeper.Keeper
addrs []sdk.AccAddress
ctx sdk.Context
}
func (suite *ParamsTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
_, addrs := app.GeneratePrivKeyAddressPairs(10)
tApp.InitializeFromGenesisStates(NewBep3GenStateMulti(tApp.AppCodec(), addrs[0]))
suite.keeper = tApp.GetBep3Keeper()
suite.ctx = ctx
suite.addrs = addrs
}
func (suite *ParamsTestSuite) TestGetSetAsset() {
asset, err := suite.keeper.GetAsset(suite.ctx, "bnb")
suite.Require().NoError(err)
suite.NotPanics(func() { suite.keeper.SetAsset(suite.ctx, asset) })
_, err = suite.keeper.GetAsset(suite.ctx, "dne")
suite.Require().Error(err)
_, err = suite.keeper.GetAsset(suite.ctx, "inc")
suite.Require().NoError(err)
}
func (suite *ParamsTestSuite) TestGetAssets() {
assets, found := suite.keeper.GetAssets(suite.ctx)
suite.Require().True(found)
suite.Require().Equal(2, len(assets))
}
func (suite *ParamsTestSuite) TestGetSetDeputyAddress() {
asset, err := suite.keeper.GetAsset(suite.ctx, "bnb")
suite.Require().NoError(err)
asset.DeputyAddress = suite.addrs[1]
suite.NotPanics(func() { suite.keeper.SetAsset(suite.ctx, asset) })
asset, err = suite.keeper.GetAsset(suite.ctx, "bnb")
suite.Require().NoError(err)
suite.Equal(suite.addrs[1], asset.DeputyAddress)
addr, err := suite.keeper.GetDeputyAddress(suite.ctx, "bnb")
suite.Require().NoError(err)
suite.Equal(suite.addrs[1], addr)
}
func (suite *ParamsTestSuite) TestGetDeputyFixedFee() {
asset, err := suite.keeper.GetAsset(suite.ctx, "bnb")
suite.Require().NoError(err)
bnbDeputyFixedFee := asset.FixedFee
res, err := suite.keeper.GetFixedFee(suite.ctx, asset.Denom)
suite.Require().NoError(err)
suite.Equal(bnbDeputyFixedFee, res)
}
func (suite *ParamsTestSuite) TestGetMinMaxSwapAmount() {
asset, err := suite.keeper.GetAsset(suite.ctx, "bnb")
suite.Require().NoError(err)
minAmount := asset.MinSwapAmount
res, err := suite.keeper.GetMinSwapAmount(suite.ctx, asset.Denom)
suite.Require().NoError(err)
suite.Equal(minAmount, res)
maxAmount := asset.MaxSwapAmount
res, err = suite.keeper.GetMaxSwapAmount(suite.ctx, asset.Denom)
suite.Require().NoError(err)
suite.Equal(maxAmount, res)
}
func (suite *ParamsTestSuite) TestGetMinMaxBlockLock() {
asset, err := suite.keeper.GetAsset(suite.ctx, "bnb")
suite.Require().NoError(err)
minLock := asset.MinBlockLock
res, err := suite.keeper.GetMinBlockLock(suite.ctx, asset.Denom)
suite.Require().NoError(err)
suite.Equal(minLock, res)
maxLock := asset.MaxBlockLock
res, err = suite.keeper.GetMaxBlockLock(suite.ctx, asset.Denom)
suite.Require().NoError(err)
suite.Equal(maxLock, res)
}
func (suite *ParamsTestSuite) TestGetAssetByCoinID() {
asset, err := suite.keeper.GetAsset(suite.ctx, "bnb")
suite.Require().NoError(err)
res, found := suite.keeper.GetAssetByCoinID(suite.ctx, asset.CoinID)
suite.True(found)
suite.Equal(asset, res)
}
func (suite *ParamsTestSuite) TestGetAuthorizedAddresses() {
deputyAddresses := suite.keeper.GetAuthorizedAddresses(suite.ctx)
// the test params use the same deputy address for two assets
expectedAddresses := []sdk.AccAddress{suite.addrs[0]}
suite.Require().ElementsMatch(expectedAddresses, deputyAddresses)
}
func (suite *AssetTestSuite) TestValidateLiveAsset() {
type args struct {
coin sdk.Coin
}
testCases := []struct {
name string
args args
expectedError error
expectPass bool
}{
{
"normal",
args{
coin: c("bnb", 1),
},
nil,
true,
},
{
"asset not supported",
args{
coin: c("bad", 1),
},
types.ErrAssetNotSupported,
false,
},
{
"asset not active",
args{
coin: c("inc", 1),
},
types.ErrAssetNotActive,
false,
},
}
for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
err := suite.keeper.ValidateLiveAsset(suite.ctx, tc.args.coin)
if tc.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(errors.Is(err, tc.expectedError))
}
})
}
}
func TestParamsTestSuite(t *testing.T) {
suite.Run(t, new(ParamsTestSuite))
}

309
x/bep3/keeper/swap.go Normal file
View File

@ -0,0 +1,309 @@
package keeper
import (
"bytes"
"encoding/hex"
"fmt"
"time"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// CreateAtomicSwap creates a new atomic swap.
func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, timestamp int64, heightSpan uint64,
sender, recipient sdk.AccAddress, senderOtherChain, recipientOtherChain string,
amount sdk.Coins, crossChain bool,
) error {
// Confirm that this is not a duplicate swap
swapID := types.CalculateSwapID(randomNumberHash, sender, senderOtherChain)
_, found := k.GetAtomicSwap(ctx, swapID)
if found {
return errorsmod.Wrap(types.ErrAtomicSwapAlreadyExists, hex.EncodeToString(swapID))
}
// Cannot send coins to a module account
if k.Maccs[recipient.String()] {
return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is a module account", recipient)
}
if len(amount) != 1 {
return fmt.Errorf("amount must contain exactly one coin")
}
asset, err := k.GetAsset(ctx, amount[0].Denom)
if err != nil {
return err
}
err = k.ValidateLiveAsset(ctx, amount[0])
if err != nil {
return err
}
// Swap amount must be within the specified swap amount limits
if amount[0].Amount.LT(asset.MinSwapAmount) || amount[0].Amount.GT(asset.MaxSwapAmount) {
return errorsmod.Wrapf(types.ErrInvalidAmount, "amount %d outside range [%s, %s]", amount[0].Amount, asset.MinSwapAmount, asset.MaxSwapAmount)
}
// Unix timestamp must be in range [-15 mins, 30 mins] of the current time
pastTimestampLimit := ctx.BlockTime().Add(time.Duration(-15) * time.Minute).Unix()
futureTimestampLimit := ctx.BlockTime().Add(time.Duration(30) * time.Minute).Unix()
if timestamp < pastTimestampLimit || timestamp >= futureTimestampLimit {
return errorsmod.Wrap(types.ErrInvalidTimestamp, fmt.Sprintf("block time: %s, timestamp: %s", ctx.BlockTime().String(), time.Unix(timestamp, 0).UTC().String()))
}
var direction types.SwapDirection
if sender.Equals(asset.DeputyAddress) {
if recipient.Equals(asset.DeputyAddress) {
return errorsmod.Wrapf(types.ErrInvalidSwapAccount, "deputy cannot be both sender and receiver: %s", asset.DeputyAddress)
}
direction = types.SWAP_DIRECTION_INCOMING
} else {
if !recipient.Equals(asset.DeputyAddress) {
return errorsmod.Wrapf(types.ErrInvalidSwapAccount, "deputy must be recipient for outgoing account: %s", recipient)
}
direction = types.SWAP_DIRECTION_OUTGOING
}
switch direction {
case types.SWAP_DIRECTION_INCOMING:
// If recipient's account doesn't exist, register it in state so that the address can send
// a claim swap tx without needing to be registered in state by receiving a coin transfer.
recipientAcc := k.accountKeeper.GetAccount(ctx, recipient)
if recipientAcc == nil {
newAcc := k.accountKeeper.NewAccountWithAddress(ctx, recipient)
k.accountKeeper.SetAccount(ctx, newAcc)
}
// Incoming swaps have already had their fees collected by the deputy during the relay process.
err = k.IncrementIncomingAssetSupply(ctx, amount[0])
case types.SWAP_DIRECTION_OUTGOING:
// Outgoing swaps must have a height span within the accepted range
if heightSpan < asset.MinBlockLock || heightSpan > asset.MaxBlockLock {
return errorsmod.Wrapf(types.ErrInvalidHeightSpan, "height span %d outside range [%d, %d]", heightSpan, asset.MinBlockLock, asset.MaxBlockLock)
}
// Amount in outgoing swaps must be able to pay the deputy's fixed fee.
if amount[0].Amount.LTE(asset.FixedFee.Add(asset.MinSwapAmount)) {
return errorsmod.Wrap(types.ErrInsufficientAmount, amount[0].String())
}
err = k.IncrementOutgoingAssetSupply(ctx, amount[0])
if err != nil {
return err
}
// Transfer coins to module - only needed for outgoing swaps
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount)
default:
err = fmt.Errorf("invalid swap direction: %s", direction.String())
}
if err != nil {
return err
}
// Store the details of the swap
expireHeight := uint64(ctx.BlockHeight()) + heightSpan
atomicSwap := types.NewAtomicSwap(amount, randomNumberHash, expireHeight, timestamp, sender,
recipient, senderOtherChain, recipientOtherChain, 0, types.SWAP_STATUS_OPEN, crossChain, direction)
// Insert the atomic swap under both keys
k.SetAtomicSwap(ctx, atomicSwap)
k.InsertIntoByBlockIndex(ctx, atomicSwap)
// Emit 'create_atomic_swap' event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCreateAtomicSwap,
sdk.NewAttribute(types.AttributeKeySender, atomicSwap.Sender.String()),
sdk.NewAttribute(types.AttributeKeyRecipient, atomicSwap.Recipient.String()),
sdk.NewAttribute(types.AttributeKeyAtomicSwapID, hex.EncodeToString(atomicSwap.GetSwapID())),
sdk.NewAttribute(types.AttributeKeyRandomNumberHash, hex.EncodeToString(atomicSwap.RandomNumberHash)),
sdk.NewAttribute(types.AttributeKeyTimestamp, fmt.Sprintf("%d", atomicSwap.Timestamp)),
sdk.NewAttribute(types.AttributeKeySenderOtherChain, atomicSwap.SenderOtherChain),
sdk.NewAttribute(types.AttributeKeyExpireHeight, fmt.Sprintf("%d", atomicSwap.ExpireHeight)),
sdk.NewAttribute(types.AttributeKeyAmount, atomicSwap.Amount.String()),
sdk.NewAttribute(types.AttributeKeyDirection, atomicSwap.Direction.String()),
),
)
return nil
}
// ClaimAtomicSwap validates a claim attempt, and if successful, sends the escrowed amount and closes the AtomicSwap.
func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte, randomNumber []byte) error {
atomicSwap, found := k.GetAtomicSwap(ctx, swapID)
if !found {
return errorsmod.Wrapf(types.ErrAtomicSwapNotFound, "%s", swapID)
}
// Only open atomic swaps can be claimed
if atomicSwap.Status != types.SWAP_STATUS_OPEN {
return errorsmod.Wrapf(types.ErrSwapNotClaimable, "status %s", atomicSwap.Status.String())
}
// Calculate hashed secret using submitted number
hashedSubmittedNumber := types.CalculateRandomHash(randomNumber, atomicSwap.Timestamp)
hashedSecret := types.CalculateSwapID(hashedSubmittedNumber, atomicSwap.Sender, atomicSwap.SenderOtherChain)
// Confirm that secret unlocks the atomic swap
if !bytes.Equal(hashedSecret, atomicSwap.GetSwapID()) {
return errorsmod.Wrapf(types.ErrInvalidClaimSecret, "the submitted random number is incorrect")
}
var err error
switch atomicSwap.Direction {
case types.SWAP_DIRECTION_INCOMING:
err = k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil {
return err
}
err = k.IncrementCurrentAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil {
return err
}
// incoming case - coins should be MINTED, then sent to user
err = k.bankKeeper.MintCoins(ctx, types.ModuleName, atomicSwap.Amount)
if err != nil {
return err
}
// Send intended recipient coins
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Recipient, atomicSwap.Amount)
if err != nil {
return err
}
case types.SWAP_DIRECTION_OUTGOING:
err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil {
return err
}
err = k.DecrementCurrentAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil {
return err
}
// outgoing case - coins should be burned
err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, atomicSwap.Amount)
if err != nil {
return err
}
default:
return fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String())
}
// Complete swap
atomicSwap.Status = types.SWAP_STATUS_COMPLETED
atomicSwap.ClosedBlock = ctx.BlockHeight()
k.SetAtomicSwap(ctx, atomicSwap)
// Remove from byBlock index and transition to longterm storage
k.RemoveFromByBlockIndex(ctx, atomicSwap)
k.InsertIntoLongtermStorage(ctx, atomicSwap)
// Emit 'claim_atomic_swap' event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaimAtomicSwap,
sdk.NewAttribute(types.AttributeKeyClaimSender, from.String()),
sdk.NewAttribute(types.AttributeKeyRecipient, atomicSwap.Recipient.String()),
sdk.NewAttribute(types.AttributeKeyAtomicSwapID, hex.EncodeToString(atomicSwap.GetSwapID())),
sdk.NewAttribute(types.AttributeKeyRandomNumberHash, hex.EncodeToString(atomicSwap.RandomNumberHash)),
sdk.NewAttribute(types.AttributeKeyRandomNumber, hex.EncodeToString(randomNumber)),
),
)
return nil
}
// RefundAtomicSwap refunds an AtomicSwap, sending assets to the original sender and closing the AtomicSwap.
func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte) error {
atomicSwap, found := k.GetAtomicSwap(ctx, swapID)
if !found {
return errorsmod.Wrapf(types.ErrAtomicSwapNotFound, "%s", swapID)
}
// Only expired swaps may be refunded
if atomicSwap.Status != types.SWAP_STATUS_EXPIRED {
return errorsmod.Wrapf(types.ErrSwapNotRefundable, "status %s", atomicSwap.Status.String())
}
var err error
switch atomicSwap.Direction {
case types.SWAP_DIRECTION_INCOMING:
err = k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0])
case types.SWAP_DIRECTION_OUTGOING:
err = k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0])
if err != nil {
return err
}
// Refund coins to original swap sender for outgoing swaps
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Sender, atomicSwap.Amount)
default:
err = fmt.Errorf("invalid swap direction: %s", atomicSwap.Direction.String())
}
if err != nil {
return err
}
// Complete swap
atomicSwap.Status = types.SWAP_STATUS_COMPLETED
atomicSwap.ClosedBlock = ctx.BlockHeight()
k.SetAtomicSwap(ctx, atomicSwap)
// Transition to longterm storage
k.InsertIntoLongtermStorage(ctx, atomicSwap)
// Emit 'refund_atomic_swap' event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeRefundAtomicSwap,
sdk.NewAttribute(types.AttributeKeyRefundSender, from.String()),
sdk.NewAttribute(types.AttributeKeySender, atomicSwap.Sender.String()),
sdk.NewAttribute(types.AttributeKeyAtomicSwapID, hex.EncodeToString(atomicSwap.GetSwapID())),
sdk.NewAttribute(types.AttributeKeyRandomNumberHash, hex.EncodeToString(atomicSwap.RandomNumberHash)),
),
)
return nil
}
// UpdateExpiredAtomicSwaps finds all AtomicSwaps that are past (or at) their ending times and expires them.
func (k Keeper) UpdateExpiredAtomicSwaps(ctx sdk.Context) {
var expiredSwapIDs []string
k.IterateAtomicSwapsByBlock(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool {
atomicSwap, found := k.GetAtomicSwap(ctx, id)
if !found {
// NOTE: shouldn't happen. Continue to next item.
return false
}
// Expire the uncompleted swap and update both indexes
atomicSwap.Status = types.SWAP_STATUS_EXPIRED
// Note: claimed swaps have already been removed from byBlock index.
k.RemoveFromByBlockIndex(ctx, atomicSwap)
k.SetAtomicSwap(ctx, atomicSwap)
expiredSwapIDs = append(expiredSwapIDs, hex.EncodeToString(atomicSwap.GetSwapID()))
return false
})
// Emit 'swaps_expired' event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSwapsExpired,
sdk.NewAttribute(types.AttributeKeyAtomicSwapIDs, fmt.Sprintf("%s", expiredSwapIDs)),
sdk.NewAttribute(types.AttributeExpirationBlock, fmt.Sprintf("%d", ctx.BlockHeight())),
),
)
}
// DeleteClosedAtomicSwapsFromLongtermStorage removes swaps one week after completion.
func (k Keeper) DeleteClosedAtomicSwapsFromLongtermStorage(ctx sdk.Context) {
k.IterateAtomicSwapsLongtermStorage(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool {
swap, found := k.GetAtomicSwap(ctx, id)
if !found {
// NOTE: shouldn't happen. Continue to next item.
return false
}
k.RemoveAtomicSwap(ctx, swap.GetSwapID())
k.RemoveFromLongtermStorage(ctx, swap)
return false
})
}

829
x/bep3/keeper/swap_test.go Normal file
View File

@ -0,0 +1,829 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
tmbytes "github.com/cometbft/cometbft/libs/bytes"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type AtomicSwapTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
randMacc sdk.AccAddress
deputy sdk.AccAddress
addrs []sdk.AccAddress
timestamps []int64
randomNumberHashes []tmbytes.HexBytes
randomNumbers []tmbytes.HexBytes
}
const (
STARING_BNB_BALANCE = int64(3000000000000)
BNB_DENOM = "bnb"
OTHER_DENOM = "inc"
STARING_OTHER_BALANCE = int64(3000000000000)
)
func (suite *AtomicSwapTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
cdc := tApp.AppCodec()
// Create and load 20 accounts with bnb tokens
coins := sdk.NewCoins(c(BNB_DENOM, STARING_BNB_BALANCE), c(OTHER_DENOM, STARING_OTHER_BALANCE))
_, addrs := app.GeneratePrivKeyAddressPairs(20)
deputy := addrs[0]
authGS := app.NewFundedGenStateWithSameCoins(tApp.AppCodec(), coins, addrs)
// Initialize genesis state
tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(cdc, deputy))
keeper := tApp.GetBep3Keeper()
params := keeper.GetParams(ctx)
params.AssetParams[1].Active = true
keeper.SetParams(ctx, params)
suite.app = tApp
suite.ctx = ctx
suite.deputy = deputy
suite.addrs = addrs
suite.keeper = keeper
// Load a random module account to test blacklisting
i := 0
var randModuleAcc sdk.AccAddress
for macc := range suite.keeper.Maccs {
if i == len(suite.keeper.Maccs)/2 {
acc, err := sdk.AccAddressFromBech32(macc)
suite.Nil(err)
randModuleAcc = acc
}
i = i + 1
}
suite.randMacc = randModuleAcc
suite.GenerateSwapDetails()
}
func (suite *AtomicSwapTestSuite) GenerateSwapDetails() {
var timestamps []int64
var randomNumberHashes []tmbytes.HexBytes
var randomNumbers []tmbytes.HexBytes
for i := 0; i < 15; i++ {
// Set up atomic swap details
timestamp := ts(i)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
timestamps = append(timestamps, timestamp)
randomNumberHashes = append(randomNumberHashes, randomNumberHash)
randomNumbers = append(randomNumbers, randomNumber[:])
}
suite.timestamps = timestamps
suite.randomNumberHashes = randomNumberHashes
suite.randomNumbers = randomNumbers
}
func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() {
currentTmTime := tmtime.Now()
type args struct {
randomNumberHash []byte
timestamp int64
heightSpan uint64
sender sdk.AccAddress
recipient sdk.AccAddress
senderOtherChain string
recipientOtherChain string
coins sdk.Coins
crossChain bool
direction types.SwapDirection
}
testCases := []struct {
name string
blockTime time.Time
args args
expectPass bool
shouldBeFound bool
}{
{
"incoming swap",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[0],
timestamp: suite.timestamps[0],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[1],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
true,
true,
},
{
"incoming swap rate limited",
currentTmTime.Add(time.Minute * 10),
args{
randomNumberHash: suite.randomNumberHashes[12],
timestamp: suite.timestamps[12],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[1],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c("inc", 50000000000)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
true,
true,
},
{
"incoming swap over rate limit",
currentTmTime.Add(time.Minute * 10),
args{
randomNumberHash: suite.randomNumberHashes[13],
timestamp: suite.timestamps[13],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[1],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c("inc", 50000000001)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
false,
},
{
"outgoing swap",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[0],
timestamp: suite.timestamps[0],
heightSpan: types.DefaultMinBlockLock,
sender: suite.addrs[1],
recipient: suite.deputy,
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_OUTGOING,
},
true,
true,
},
{
"outgoing swap amount not greater than fixed fee",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[1],
timestamp: suite.timestamps[1],
heightSpan: types.DefaultMinBlockLock,
sender: suite.addrs[1],
recipient: suite.addrs[2],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 1000)),
crossChain: true,
direction: types.SWAP_DIRECTION_OUTGOING,
},
false,
false,
},
{
"unsupported asset",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[2],
timestamp: suite.timestamps[2],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[2],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c("xyz", 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
false,
},
{
"outside timestamp range",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[3],
timestamp: suite.timestamps[3] - 2000,
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[3],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
false,
},
{
"future timestamp",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[4],
timestamp: suite.timestamps[4] + 5000,
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[4],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
false,
},
{
"small height span on outgoing swap",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[5],
timestamp: suite.timestamps[5],
heightSpan: uint64(100),
sender: suite.addrs[5],
recipient: suite.deputy,
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_OUTGOING,
},
false,
false,
},
{
"big height span on outgoing swap",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[6],
timestamp: suite.timestamps[6],
heightSpan: uint64(300),
sender: suite.addrs[6],
recipient: suite.deputy,
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_OUTGOING,
},
false,
false,
},
{
"zero amount",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[7],
timestamp: suite.timestamps[7],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[7],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 0)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
false,
},
{
"duplicate swap",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[0],
timestamp: suite.timestamps[0],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[1],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 50000)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
true,
},
{
"recipient is module account",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[8],
timestamp: suite.timestamps[8],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.randMacc,
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 5000)),
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
false,
},
{
"exactly at maximum amount",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[10],
timestamp: suite.timestamps[10],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[4],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 1000000000000)), // 10,000 BNB
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
true,
true,
},
{
"above maximum amount",
currentTmTime,
args{
randomNumberHash: suite.randomNumberHashes[11],
timestamp: suite.timestamps[11],
heightSpan: types.DefaultMinBlockLock,
sender: suite.deputy,
recipient: suite.addrs[5],
senderOtherChain: TestSenderOtherChain,
recipientOtherChain: TestRecipientOtherChain,
coins: cs(c(BNB_DENOM, 1000000000001)), // 10,001 BNB
crossChain: true,
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Increment current asset supply to support outgoing swaps
suite.ctx = suite.ctx.WithBlockTime(tc.blockTime)
if tc.args.direction == types.SWAP_DIRECTION_OUTGOING {
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coins[0])
suite.Nil(err)
}
// Load asset denom (required for zero coins test case)
var swapAssetDenom string
if len(tc.args.coins) == 1 {
swapAssetDenom = tc.args.coins[0].Denom
} else {
swapAssetDenom = BNB_DENOM
}
// Load sender's account prior to swap creation
bk := suite.app.GetBankKeeper()
senderBalancePre := bk.GetBalance(suite.ctx, tc.args.sender, swapAssetDenom)
assetSupplyPre, _ := suite.keeper.GetAssetSupply(suite.ctx, swapAssetDenom)
// Create atomic swap
err := suite.keeper.CreateAtomicSwap(suite.ctx, tc.args.randomNumberHash, tc.args.timestamp,
tc.args.heightSpan, tc.args.sender, tc.args.recipient, tc.args.senderOtherChain,
tc.args.recipientOtherChain, tc.args.coins, tc.args.crossChain)
// Load sender's account after swap creation
senderBalancePost := bk.GetBalance(suite.ctx, tc.args.sender, swapAssetDenom)
assetSupplyPost, _ := suite.keeper.GetAssetSupply(suite.ctx, swapAssetDenom)
// Load expected swap ID
expectedSwapID := types.CalculateSwapID(tc.args.randomNumberHash, tc.args.sender, tc.args.senderOtherChain)
if tc.expectPass {
suite.NoError(err)
// Check incoming/outgoing asset supply increased
switch tc.args.direction {
case types.SWAP_DIRECTION_INCOMING:
suite.Equal(assetSupplyPre.IncomingSupply.Add(tc.args.coins[0]), assetSupplyPost.IncomingSupply)
case types.SWAP_DIRECTION_OUTGOING:
// Check coins moved
suite.Equal(senderBalancePre.Sub(tc.args.coins[0]), senderBalancePost)
suite.Equal(assetSupplyPre.OutgoingSupply.Add(tc.args.coins[0]), assetSupplyPost.OutgoingSupply)
default:
suite.Fail("should not have invalid direction")
}
// Check swap in store
actualSwap, found := suite.keeper.GetAtomicSwap(suite.ctx, expectedSwapID)
suite.True(found)
suite.NotNil(actualSwap)
// Confirm swap contents
expectedSwap := types.AtomicSwap{
Amount: tc.args.coins,
RandomNumberHash: tc.args.randomNumberHash,
ExpireHeight: uint64(suite.ctx.BlockHeight()) + tc.args.heightSpan,
Timestamp: tc.args.timestamp,
Sender: tc.args.sender,
Recipient: tc.args.recipient,
SenderOtherChain: tc.args.senderOtherChain,
RecipientOtherChain: tc.args.recipientOtherChain,
ClosedBlock: 0,
Status: types.SWAP_STATUS_OPEN,
CrossChain: tc.args.crossChain,
Direction: tc.args.direction,
}
suite.Equal(expectedSwap, actualSwap)
} else {
suite.Error(err)
// Check coins not moved
suite.Equal(senderBalancePre, senderBalancePost)
// Check incoming/outgoing asset supply not increased
switch tc.args.direction {
case types.SWAP_DIRECTION_INCOMING:
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
case types.SWAP_DIRECTION_OUTGOING:
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
default:
suite.Fail("should not have invalid direction")
}
// Check if swap found in store
_, found := suite.keeper.GetAtomicSwap(suite.ctx, expectedSwapID)
if !tc.shouldBeFound {
suite.False(found)
} else {
suite.True(found)
}
}
})
}
}
func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() {
suite.SetupTest()
currentTmTime := tmtime.Now()
invalidRandomNumber, _ := types.GenerateSecureRandomNumber()
type args struct {
coins sdk.Coins
swapID []byte
randomNumber []byte
direction types.SwapDirection
}
testCases := []struct {
name string
claimCtx sdk.Context
args args
expectPass bool
}{
{
"normal incoming swap",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.SWAP_DIRECTION_INCOMING,
},
true,
},
{
"normal incoming swap rate-limited",
suite.ctx.WithBlockTime(currentTmTime.Add(time.Minute * 10)),
args{
coins: cs(c(OTHER_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.SWAP_DIRECTION_INCOMING,
},
true,
},
{
"normal outgoing swap",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.SWAP_DIRECTION_OUTGOING,
},
true,
},
{
"invalid random number",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: invalidRandomNumber[:],
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
},
{
"wrong swap ID",
suite.ctx,
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: types.CalculateSwapID(suite.randomNumberHashes[3], suite.addrs[6], TestRecipientOtherChain),
randomNumber: []byte{},
direction: types.SWAP_DIRECTION_OUTGOING,
},
false,
},
{
"past expiration",
suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 2000),
args{
coins: cs(c(BNB_DENOM, 50000)),
swapID: []byte{},
randomNumber: []byte{},
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
},
}
for i, tc := range testCases {
suite.GenerateSwapDetails()
suite.Run(tc.name, func() {
expectedRecipient := suite.addrs[5]
sender := suite.deputy
// Set sender to other and increment current asset supply for outgoing swap
if tc.args.direction == types.SWAP_DIRECTION_OUTGOING {
sender = suite.addrs[6]
expectedRecipient = suite.deputy
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coins[0])
suite.Nil(err)
}
// Create atomic swap
err := suite.keeper.CreateAtomicSwap(suite.ctx, suite.randomNumberHashes[i], suite.timestamps[i],
types.DefaultMinBlockLock, sender, expectedRecipient, TestSenderOtherChain, TestRecipientOtherChain,
tc.args.coins, true)
suite.NoError(err)
realSwapID := types.CalculateSwapID(suite.randomNumberHashes[i], sender, TestSenderOtherChain)
// If args contains an invalid swap ID claim attempt will use it instead of the real swap ID
var claimSwapID []byte
if len(tc.args.swapID) == 0 {
claimSwapID = realSwapID
} else {
claimSwapID = tc.args.swapID
}
// If args contains an invalid random number claim attempt will use it instead of the real random number
var claimRandomNumber []byte
if len(tc.args.randomNumber) == 0 {
claimRandomNumber = suite.randomNumbers[i]
} else {
claimRandomNumber = tc.args.randomNumber
}
// Run the beginblocker before attempting claim
bep3.BeginBlocker(tc.claimCtx, suite.keeper)
// Load expected recipient's account prior to claim attempt
bk := suite.app.GetBankKeeper()
expectedRecipientBalancePre := bk.GetBalance(suite.ctx, expectedRecipient, tc.args.coins[0].Denom)
// Load asset supplies prior to claim attempt
assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.claimCtx, tc.args.coins[0].Denom)
// Attempt to claim atomic swap
err = suite.keeper.ClaimAtomicSwap(tc.claimCtx, expectedRecipient, claimSwapID, claimRandomNumber)
// Load expected recipient's account after the claim attempt
expectedRecipientBalancePost := bk.GetBalance(suite.ctx, expectedRecipient, tc.args.coins[0].Denom)
// Load asset supplies after the claim attempt
assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.claimCtx, tc.args.coins[0].Denom)
if tc.expectPass {
suite.NoError(err)
// Check asset supply changes
switch tc.args.direction {
case types.SWAP_DIRECTION_INCOMING:
// Check coins moved
suite.Equal(expectedRecipientBalancePre.Add(tc.args.coins[0]), expectedRecipientBalancePost)
// Check incoming supply decreased
suite.True(assetSupplyPre.IncomingSupply.Amount.Sub(tc.args.coins[0].Amount).Equal(assetSupplyPost.IncomingSupply.Amount))
// Check current supply increased
suite.Equal(assetSupplyPre.CurrentSupply.Add(tc.args.coins[0]), assetSupplyPost.CurrentSupply)
// Check outgoing supply not changed
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
case types.SWAP_DIRECTION_OUTGOING:
// Check incoming supply not changed
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
// Check current supply decreased
suite.Equal(assetSupplyPre.CurrentSupply.Sub(tc.args.coins[0]), assetSupplyPost.CurrentSupply)
// Check outgoing supply decreased
suite.True(assetSupplyPre.OutgoingSupply.Sub(tc.args.coins[0]).IsEqual(assetSupplyPost.OutgoingSupply))
default:
suite.Fail("should not have invalid direction")
}
} else {
suite.Error(err)
// Check coins not moved
suite.Equal(expectedRecipientBalancePre, expectedRecipientBalancePost)
// Check asset supply has not changed
switch tc.args.direction {
case types.SWAP_DIRECTION_INCOMING:
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
case types.SWAP_DIRECTION_OUTGOING:
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
default:
suite.Fail("should not have invalid direction")
}
}
})
}
}
func (suite *AtomicSwapTestSuite) TestRefundAtomicSwap() {
suite.SetupTest()
type args struct {
swapID []byte
direction types.SwapDirection
}
testCases := []struct {
name string
refundCtx sdk.Context
args args
expectPass bool
}{
{
"normal incoming swap",
suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
args{
swapID: []byte{},
direction: types.SWAP_DIRECTION_INCOMING,
},
true,
},
{
"normal outgoing swap",
suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400),
args{
swapID: []byte{},
direction: types.SWAP_DIRECTION_OUTGOING,
},
true,
},
{
"before expiration",
suite.ctx,
args{
swapID: []byte{},
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
},
{
"wrong swapID",
suite.ctx,
args{
swapID: types.CalculateSwapID(suite.randomNumberHashes[6], suite.addrs[1], TestRecipientOtherChain),
direction: types.SWAP_DIRECTION_INCOMING,
},
false,
},
}
for i, tc := range testCases {
suite.GenerateSwapDetails()
suite.Run(tc.name, func() {
// Create atomic swap
expectedRefundAmount := cs(c(BNB_DENOM, 50000))
sender := suite.deputy
expectedRecipient := suite.addrs[9]
// Set sender to other and increment current asset supply for outgoing swap
if tc.args.direction == types.SWAP_DIRECTION_OUTGOING {
sender = suite.addrs[6]
expectedRecipient = suite.deputy
err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, expectedRefundAmount[0])
suite.Nil(err)
}
err := suite.keeper.CreateAtomicSwap(suite.ctx, suite.randomNumberHashes[i], suite.timestamps[i],
types.DefaultMinBlockLock, sender, expectedRecipient, TestSenderOtherChain, TestRecipientOtherChain,
expectedRefundAmount, true)
suite.NoError(err)
realSwapID := types.CalculateSwapID(suite.randomNumberHashes[i], sender, TestSenderOtherChain)
// If args contains an invalid swap ID refund attempt will use it instead of the real swap ID
var refundSwapID []byte
if len(tc.args.swapID) == 0 {
refundSwapID = realSwapID
} else {
refundSwapID = tc.args.swapID
}
// Run the beginblocker before attempting refund
bep3.BeginBlocker(tc.refundCtx, suite.keeper)
// Load sender's account prior to swap refund
bk := suite.app.GetBankKeeper()
originalSenderBalancePre := bk.GetBalance(suite.ctx, sender, expectedRefundAmount[0].Denom)
// Load asset supply prior to swap refund
assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.refundCtx, expectedRefundAmount[0].Denom)
// Attempt to refund atomic swap
err = suite.keeper.RefundAtomicSwap(tc.refundCtx, sender, refundSwapID)
// Load sender's account after refund
originalSenderBalancePost := bk.GetBalance(suite.ctx, sender, expectedRefundAmount[0].Denom)
// Load asset supply after to swap refund
assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.refundCtx, expectedRefundAmount[0].Denom)
if tc.expectPass {
suite.NoError(err)
// Check asset supply changes
switch tc.args.direction {
case types.SWAP_DIRECTION_INCOMING:
// Check incoming supply decreased
suite.True(assetSupplyPre.IncomingSupply.Sub(expectedRefundAmount[0]).IsEqual(assetSupplyPost.IncomingSupply))
// Check current, outgoing supply not changed
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
case types.SWAP_DIRECTION_OUTGOING:
// Check coins moved
suite.Equal(originalSenderBalancePre.Add(expectedRefundAmount[0]), originalSenderBalancePost)
// Check incoming, current supply not changed
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
// Check outgoing supply decreased
suite.True(assetSupplyPre.OutgoingSupply.Sub(expectedRefundAmount[0]).IsEqual(assetSupplyPost.OutgoingSupply))
default:
suite.Fail("should not have invalid direction")
}
} else {
suite.Error(err)
// Check coins not moved
suite.Equal(originalSenderBalancePre, originalSenderBalancePost)
// Check asset supply has not changed
switch tc.args.direction {
case types.SWAP_DIRECTION_INCOMING:
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
case types.SWAP_DIRECTION_OUTGOING:
suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply)
suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply)
suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply)
default:
suite.Fail("should not have invalid direction")
}
}
})
}
}
func TestAtomicSwapTestSuite(t *testing.T) {
suite.Run(t, new(AtomicSwapTestSuite))
}

View File

@ -0,0 +1,57 @@
package v0_16
import (
"fmt"
"github.com/0glabs/0g-chain/x/bep3/types"
)
// resetSwapForZeroHeight updates swap expiry/close heights to work when the chain height is reset to zero.
func resetSwapForZeroHeight(swap types.AtomicSwap) types.AtomicSwap {
switch status := swap.Status; status {
case types.SWAP_STATUS_COMPLETED:
// Reset closed block to one so completed swaps are not held in long term storage too long.
swap.ClosedBlock = 1
case types.SWAP_STATUS_OPEN:
switch dir := swap.Direction; dir {
case types.SWAP_DIRECTION_INCOMING:
// Open incoming swaps can be expired safely. They haven't been claimed yet, so the outgoing swap on bnb will just timeout.
// The chain downtime cannot be accurately predicted, so it's easier to expire than to recalculate a correct expire height.
swap.ExpireHeight = 1
swap.Status = types.SWAP_STATUS_EXPIRED
case types.SWAP_DIRECTION_OUTGOING:
// Open outgoing swaps should be extended to allow enough time to claim after the chain launches.
// They cannot be expired as there could be an open/claimed bnb swap.
swap.ExpireHeight = 1 + 24686 // default timeout used when sending swaps from 0g
case types.SWAP_DIRECTION_UNSPECIFIED:
default:
panic(fmt.Sprintf("unknown bep3 swap direction '%s'", dir))
}
case types.SWAP_STATUS_EXPIRED:
// Once a swap is marked expired the expire height is ignored. However reset to 1 to be sure.
swap.ExpireHeight = 1
case types.SWAP_STATUS_UNSPECIFIED:
default:
panic(fmt.Sprintf("unknown bep3 swap status '%s'", status))
}
return swap
}
func resetSwapsForZeroHeight(oldSwaps types.AtomicSwaps) types.AtomicSwaps {
newSwaps := make(types.AtomicSwaps, len(oldSwaps))
for i, oldSwap := range oldSwaps {
swap := resetSwapForZeroHeight(oldSwap)
newSwaps[i] = swap
}
return newSwaps
}
func Migrate(oldState types.GenesisState) *types.GenesisState {
return &types.GenesisState{
PreviousBlockTime: oldState.PreviousBlockTime,
Params: oldState.Params,
AtomicSwaps: resetSwapsForZeroHeight(oldState.AtomicSwaps),
Supplies: oldState.Supplies,
}
}

View File

@ -0,0 +1,175 @@
package v0_16
import (
"io/ioutil"
"path/filepath"
"testing"
"time"
sdkmath "cosmossdk.io/math"
"github.com/cometbft/cometbft/libs/bytes"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
app "github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type migrateTestSuite struct {
suite.Suite
addresses []sdk.AccAddress
v16genstate types.GenesisState
cdc codec.Codec
}
func (s *migrateTestSuite) SetupTest() {
app.SetSDKConfig()
s.v16genstate = types.GenesisState{
PreviousBlockTime: time.Date(2021, 4, 8, 15, 0, 0, 0, time.UTC),
Params: types.Params{},
Supplies: types.AssetSupplies{},
AtomicSwaps: types.AtomicSwaps{},
}
config := app.MakeEncodingConfig()
s.cdc = config.Marshaler
_, accAddresses := app.GeneratePrivKeyAddressPairs(10)
s.addresses = accAddresses
}
func (s *migrateTestSuite) TestMigrate_JSON() {
// Migrate v16 bep3 to v17
file := filepath.Join("testdata", "v16-bep3.json")
data, err := ioutil.ReadFile(file)
s.Require().NoError(err)
err = s.cdc.UnmarshalJSON(data, &s.v16genstate)
s.Require().NoError(err)
genstate := Migrate(s.v16genstate)
// Compare expect v16 bep3 json with migrated json
actual := s.cdc.MustMarshalJSON(genstate)
file = filepath.Join("testdata", "v17-bep3.json")
expected, err := ioutil.ReadFile(file)
s.Require().NoError(err)
s.Require().JSONEq(string(expected), string(actual))
}
func (s *migrateTestSuite) TestMigrate_Swaps() {
type swap struct {
ExpireHeight uint64
CloseBlock int64
Status types.SwapStatus
Direction types.SwapDirection
}
testcases := []struct {
name string
oldSwap swap
newSwap swap
}{
{
name: "incoming open swap",
oldSwap: swap{
// expire and close not set in open swaps
Status: types.SWAP_STATUS_OPEN,
Direction: types.SWAP_DIRECTION_INCOMING,
},
newSwap: swap{
ExpireHeight: 1,
Status: types.SWAP_STATUS_EXPIRED,
Direction: types.SWAP_DIRECTION_INCOMING,
},
},
{
name: "outgoing open swap",
oldSwap: swap{
// expire and close not set in open swaps
Status: types.SWAP_STATUS_OPEN,
Direction: types.SWAP_DIRECTION_OUTGOING,
},
newSwap: swap{
ExpireHeight: 24687,
Status: types.SWAP_STATUS_OPEN,
Direction: types.SWAP_DIRECTION_OUTGOING,
},
},
{
name: "completed swap",
oldSwap: swap{
ExpireHeight: 1000,
CloseBlock: 900,
Status: types.SWAP_STATUS_COMPLETED,
Direction: types.SWAP_DIRECTION_INCOMING,
},
newSwap: swap{
ExpireHeight: 1000,
CloseBlock: 1,
Status: types.SWAP_STATUS_COMPLETED,
Direction: types.SWAP_DIRECTION_INCOMING,
},
},
{
name: "expired swap",
oldSwap: swap{
ExpireHeight: 1000,
CloseBlock: 900,
Status: types.SWAP_STATUS_EXPIRED,
Direction: types.SWAP_DIRECTION_INCOMING,
},
newSwap: swap{
ExpireHeight: 1,
CloseBlock: 900,
Status: types.SWAP_STATUS_EXPIRED,
Direction: types.SWAP_DIRECTION_INCOMING,
},
},
}
for _, tc := range testcases {
s.Run(tc.name, func() {
oldSwaps := types.AtomicSwaps{
{
Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdkmath.NewInt(12))),
RandomNumberHash: bytes.HexBytes{},
ExpireHeight: tc.oldSwap.ExpireHeight,
Timestamp: 1110,
Sender: s.addresses[0],
Recipient: s.addresses[1],
RecipientOtherChain: s.addresses[0].String(),
SenderOtherChain: s.addresses[1].String(),
ClosedBlock: tc.oldSwap.CloseBlock,
Status: tc.oldSwap.Status,
CrossChain: true,
Direction: tc.oldSwap.Direction,
},
}
expectedSwaps := types.AtomicSwaps{
{
Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdkmath.NewInt(12))),
RandomNumberHash: bytes.HexBytes{},
ExpireHeight: tc.newSwap.ExpireHeight,
Timestamp: 1110,
Sender: s.addresses[0],
Recipient: s.addresses[1],
RecipientOtherChain: s.addresses[0].String(),
SenderOtherChain: s.addresses[1].String(),
ClosedBlock: tc.newSwap.CloseBlock,
Status: tc.newSwap.Status,
CrossChain: true,
Direction: tc.newSwap.Direction,
},
}
s.v16genstate.AtomicSwaps = oldSwaps
genState := Migrate(s.v16genstate)
s.Require().Len(genState.AtomicSwaps, 1)
s.Equal(expectedSwaps, genState.AtomicSwaps)
})
}
}
func TestMigrateTestSuite(t *testing.T) {
suite.Run(t, new(migrateTestSuite))
}

View File

@ -0,0 +1,212 @@
{
"atomic_swaps": [
{
"amount": [
{
"amount": "1999955998",
"denom": "btcb"
}
],
"closed_block": "838115",
"cross_chain": true,
"direction": "SWAP_DIRECTION_INCOMING",
"expire_height": "838627",
"random_number_hash": "6F1CF8F2E13A0C0F0A359F54E47E4E265D766B8E006D2F00BDF994ABDEF1E9E4",
"recipient": "kava1fl2hs6y9vz986g5v52pdan9ga923n9mn5cxxkw",
"recipient_other_chain": "bnb1xz3xqf4p2ygrw9lhp5g5df4ep4nd20vsywnmpr",
"sender": "kava14qsmvzprqvhwmgql9fr0u3zv9n2qla8zhnm5pc",
"sender_other_chain": "bnb19k9wuv2j7c7ck8tmc7kav0r0cnt3esmkrpf25x",
"status": "SWAP_STATUS_COMPLETED",
"timestamp": "1636034914"
},
{
"amount": [
{
"amount": "19000000000",
"denom": "bnb"
}
],
"closed_block": "1712118",
"cross_chain": true,
"direction": "SWAP_DIRECTION_OUTGOING",
"expire_height": "1736797",
"random_number_hash": "280EB832A37F2265CC82F3957CE603AAD57BAD7038B876A1F28953AFA29FA1C3",
"recipient": "kava1r4v2zdhdalfj2ydazallqvrus9fkphmglhn6u6",
"recipient_other_chain": "bnb18nsgj50zvc4uq93w4j0ltz5gaxhwv7aq4qnq0p",
"sender": "kava1zw6gg4ztvly7zf25pa33mclav3spvj3ympxxna",
"sender_other_chain": "bnb1jh7uv2rm6339yue8k4mj9406k3509kr4wt5nxn",
"status": "SWAP_STATUS_COMPLETED",
"timestamp": "1641976566"
},
{
"amount": [
{
"amount": "999595462080",
"denom": "busd"
}
],
"closed_block": "787122",
"cross_chain": true,
"direction": "SWAP_DIRECTION_INCOMING",
"expire_height": "811799",
"random_number_hash": "BFB7CC82DA0E0C8556AC37843F5AB136B9A7A066054368F5948944282B414D83",
"recipient": "kava1eufgf0w9d7hf5mgtek4zr2upkxag9stmzx6unl",
"recipient_other_chain": "bnb10zq89008gmedc6rrwzdfukjk94swynd7dl97w8",
"sender": "kava1hh4x3a4suu5zyaeauvmv7ypf7w9llwlfufjmuu",
"sender_other_chain": "bnb1vl3wn4x8kqajg2j9wxa5y5amgzdxchutkxr6at",
"status": "SWAP_STATUS_EXPIRED",
"timestamp": "1635694492"
},
{
"amount": [
{
"amount": "999595462080",
"denom": "busd"
}
],
"closed_block": "787122",
"cross_chain": true,
"direction": "SWAP_DIRECTION_OUTGOING",
"expire_height": "811799",
"random_number_hash": "BFB7CC82DA0E0C8556AC37843F5AB136B9A7A066054368F5948944282B414D83",
"recipient": "kava1hh4x3a4suu5zyaeauvmv7ypf7w9llwlfufjmuu",
"recipient_other_chain": "bnb1vl3wn4x8kqajg2j9wxa5y5amgzdxchutkxr6at",
"sender": "kava1eufgf0w9d7hf5mgtek4zr2upkxag9stmzx6unl",
"sender_other_chain": "bnb10zq89008gmedc6rrwzdfukjk94swynd7dl97w8",
"status": "SWAP_STATUS_EXPIRED",
"timestamp": "1635694492"
},
{
"amount": [
{
"amount": "1000000",
"denom": "btcb"
}
],
"closed_block": "0",
"cross_chain": true,
"direction": "SWAP_DIRECTION_OUTGOING",
"expire_height": "1730589",
"random_number_hash": "A74EA1AB58D312FDF1E872D18583CACCF294E639DDA4F303939E9ADCEC081D93",
"recipient": "kava14qsmvzprqvhwmgql9fr0u3zv9n2qla8zhnm5pc",
"recipient_other_chain": "bnb1lhk5ndlgf5wz55t8k35cqj6h9l3m4l5ek2w7q6",
"sender": "kava1d2u28azje7rhqyjtxc2ex8q0cxxpw7dfm7ltq5",
"sender_other_chain": "bnb1xz3xqf4p2ygrw9lhp5g5df4ep4nd20vsywnmpr",
"status": "SWAP_STATUS_OPEN",
"timestamp": "1641934114"
},
{
"amount": [
{
"amount": "1000000",
"denom": "btcb"
}
],
"closed_block": "0",
"cross_chain": true,
"direction": "SWAP_DIRECTION_INCOMING",
"expire_height": "1740000",
"random_number_hash": "39E9ADCEC081D93A74EA1A83CACCF294E639DDA4F3039B58D312FDF1E872D185",
"recipient": "kava1d2u28azje7rhqyjtxc2ex8q0cxxpw7dfm7ltq5",
"recipient_other_chain": "bnb1xz3xqf4p2ygrw9lhp5g5df4ep4nd20vsywnmpr",
"sender": "kava14qsmvzprqvhwmgql9fr0u3zv9n2qla8zhnm5pc",
"sender_other_chain": "bnb1lhk5ndlgf5wz55t8k35cqj6h9l3m4l5ek2w7q6",
"status": "SWAP_STATUS_OPEN",
"timestamp": "1641934114"
}
],
"params": {
"asset_params": [
{
"active": true,
"coin_id": "0",
"denom": "btcb",
"deputy_address": "kava1kla4wl0ccv7u85cemvs3y987hqk0afcv7vue84",
"fixed_fee": "2",
"max_block_lock": "86400",
"max_swap_amount": "2000000000",
"min_block_lock": "24686",
"min_swap_amount": "3",
"supply_limit": {
"limit": "100000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
},
{
"active": true,
"coin_id": "144",
"denom": "xrpb",
"deputy_address": "kava14q5sawxdxtpap5x5sgzj7v4sp3ucncjlpuk3hs",
"fixed_fee": "100000",
"max_block_lock": "86400",
"max_swap_amount": "250000000000000",
"min_block_lock": "24686",
"min_swap_amount": "100001",
"supply_limit": {
"limit": "2000000000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
},
{
"active": true,
"coin_id": "714",
"denom": "bnb",
"deputy_address": "kava1agcvt07tcw0tglu0hmwdecsnuxp2yd45f3avgm",
"fixed_fee": "1000",
"max_block_lock": "86400",
"max_swap_amount": "500000000000",
"min_block_lock": "24686",
"min_swap_amount": "1001",
"supply_limit": {
"limit": "100000000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
},
{
"active": true,
"coin_id": "727",
"denom": "busd",
"deputy_address": "kava1j9je7f6s0v6k7dmgv6u5k5ru202f5ffsc7af04",
"fixed_fee": "20000",
"max_block_lock": "86400",
"max_swap_amount": "100000000000000",
"min_block_lock": "24686",
"min_swap_amount": "20001",
"supply_limit": {
"limit": "2000000000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
}
]
},
"previous_block_time": "1970-01-01T00:00:00Z",
"supplies": [
{
"current_supply": {
"amount": "30467559434006",
"denom": "bnb"
},
"incoming_supply": {
"amount": "0",
"denom": "bnb"
},
"outgoing_supply": {
"amount": "0",
"denom": "bnb"
},
"time_elapsed": "0s",
"time_limited_current_supply": {
"amount": "0",
"denom": "bnb"
}
}
]
}

View File

@ -0,0 +1,212 @@
{
"atomic_swaps": [
{
"amount": [
{
"amount": "1999955998",
"denom": "btcb"
}
],
"closed_block": "1",
"cross_chain": true,
"direction": "SWAP_DIRECTION_INCOMING",
"expire_height": "838627",
"random_number_hash": "6F1CF8F2E13A0C0F0A359F54E47E4E265D766B8E006D2F00BDF994ABDEF1E9E4",
"recipient": "kava1fl2hs6y9vz986g5v52pdan9ga923n9mn5cxxkw",
"recipient_other_chain": "bnb1xz3xqf4p2ygrw9lhp5g5df4ep4nd20vsywnmpr",
"sender": "kava14qsmvzprqvhwmgql9fr0u3zv9n2qla8zhnm5pc",
"sender_other_chain": "bnb19k9wuv2j7c7ck8tmc7kav0r0cnt3esmkrpf25x",
"status": "SWAP_STATUS_COMPLETED",
"timestamp": "1636034914"
},
{
"amount": [
{
"amount": "19000000000",
"denom": "bnb"
}
],
"closed_block": "1",
"cross_chain": true,
"direction": "SWAP_DIRECTION_OUTGOING",
"expire_height": "1736797",
"random_number_hash": "280EB832A37F2265CC82F3957CE603AAD57BAD7038B876A1F28953AFA29FA1C3",
"recipient": "kava1r4v2zdhdalfj2ydazallqvrus9fkphmglhn6u6",
"recipient_other_chain": "bnb18nsgj50zvc4uq93w4j0ltz5gaxhwv7aq4qnq0p",
"sender": "kava1zw6gg4ztvly7zf25pa33mclav3spvj3ympxxna",
"sender_other_chain": "bnb1jh7uv2rm6339yue8k4mj9406k3509kr4wt5nxn",
"status": "SWAP_STATUS_COMPLETED",
"timestamp": "1641976566"
},
{
"amount": [
{
"amount": "999595462080",
"denom": "busd"
}
],
"closed_block": "787122",
"cross_chain": true,
"direction": "SWAP_DIRECTION_INCOMING",
"expire_height": "1",
"random_number_hash": "BFB7CC82DA0E0C8556AC37843F5AB136B9A7A066054368F5948944282B414D83",
"recipient": "kava1eufgf0w9d7hf5mgtek4zr2upkxag9stmzx6unl",
"recipient_other_chain": "bnb10zq89008gmedc6rrwzdfukjk94swynd7dl97w8",
"sender": "kava1hh4x3a4suu5zyaeauvmv7ypf7w9llwlfufjmuu",
"sender_other_chain": "bnb1vl3wn4x8kqajg2j9wxa5y5amgzdxchutkxr6at",
"status": "SWAP_STATUS_EXPIRED",
"timestamp": "1635694492"
},
{
"amount": [
{
"amount": "999595462080",
"denom": "busd"
}
],
"closed_block": "787122",
"cross_chain": true,
"direction": "SWAP_DIRECTION_OUTGOING",
"expire_height": "1",
"random_number_hash": "BFB7CC82DA0E0C8556AC37843F5AB136B9A7A066054368F5948944282B414D83",
"recipient": "kava1hh4x3a4suu5zyaeauvmv7ypf7w9llwlfufjmuu",
"recipient_other_chain": "bnb1vl3wn4x8kqajg2j9wxa5y5amgzdxchutkxr6at",
"sender": "kava1eufgf0w9d7hf5mgtek4zr2upkxag9stmzx6unl",
"sender_other_chain": "bnb10zq89008gmedc6rrwzdfukjk94swynd7dl97w8",
"status": "SWAP_STATUS_EXPIRED",
"timestamp": "1635694492"
},
{
"amount": [
{
"amount": "1000000",
"denom": "btcb"
}
],
"closed_block": "0",
"cross_chain": true,
"direction": "SWAP_DIRECTION_OUTGOING",
"expire_height": "24687",
"random_number_hash": "A74EA1AB58D312FDF1E872D18583CACCF294E639DDA4F303939E9ADCEC081D93",
"recipient": "kava14qsmvzprqvhwmgql9fr0u3zv9n2qla8zhnm5pc",
"recipient_other_chain": "bnb1lhk5ndlgf5wz55t8k35cqj6h9l3m4l5ek2w7q6",
"sender": "kava1d2u28azje7rhqyjtxc2ex8q0cxxpw7dfm7ltq5",
"sender_other_chain": "bnb1xz3xqf4p2ygrw9lhp5g5df4ep4nd20vsywnmpr",
"status": "SWAP_STATUS_OPEN",
"timestamp": "1641934114"
},
{
"amount": [
{
"amount": "1000000",
"denom": "btcb"
}
],
"closed_block": "0",
"cross_chain": true,
"direction": "SWAP_DIRECTION_INCOMING",
"expire_height": "1",
"random_number_hash": "39E9ADCEC081D93A74EA1A83CACCF294E639DDA4F3039B58D312FDF1E872D185",
"recipient": "kava1d2u28azje7rhqyjtxc2ex8q0cxxpw7dfm7ltq5",
"recipient_other_chain": "bnb1xz3xqf4p2ygrw9lhp5g5df4ep4nd20vsywnmpr",
"sender": "kava14qsmvzprqvhwmgql9fr0u3zv9n2qla8zhnm5pc",
"sender_other_chain": "bnb1lhk5ndlgf5wz55t8k35cqj6h9l3m4l5ek2w7q6",
"status": "SWAP_STATUS_EXPIRED",
"timestamp": "1641934114"
}
],
"params": {
"asset_params": [
{
"active": true,
"coin_id": "0",
"denom": "btcb",
"deputy_address": "kava1kla4wl0ccv7u85cemvs3y987hqk0afcv7vue84",
"fixed_fee": "2",
"max_block_lock": "86400",
"max_swap_amount": "2000000000",
"min_block_lock": "24686",
"min_swap_amount": "3",
"supply_limit": {
"limit": "100000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
},
{
"active": true,
"coin_id": "144",
"denom": "xrpb",
"deputy_address": "kava14q5sawxdxtpap5x5sgzj7v4sp3ucncjlpuk3hs",
"fixed_fee": "100000",
"max_block_lock": "86400",
"max_swap_amount": "250000000000000",
"min_block_lock": "24686",
"min_swap_amount": "100001",
"supply_limit": {
"limit": "2000000000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
},
{
"active": true,
"coin_id": "714",
"denom": "bnb",
"deputy_address": "kava1agcvt07tcw0tglu0hmwdecsnuxp2yd45f3avgm",
"fixed_fee": "1000",
"max_block_lock": "86400",
"max_swap_amount": "500000000000",
"min_block_lock": "24686",
"min_swap_amount": "1001",
"supply_limit": {
"limit": "100000000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
},
{
"active": true,
"coin_id": "727",
"denom": "busd",
"deputy_address": "kava1j9je7f6s0v6k7dmgv6u5k5ru202f5ffsc7af04",
"fixed_fee": "20000",
"max_block_lock": "86400",
"max_swap_amount": "100000000000000",
"min_block_lock": "24686",
"min_swap_amount": "20001",
"supply_limit": {
"limit": "2000000000000000",
"time_based_limit": "0",
"time_limited": false,
"time_period": "0s"
}
}
]
},
"previous_block_time": "1970-01-01T00:00:00Z",
"supplies": [
{
"current_supply": {
"amount": "30467559434006",
"denom": "bnb"
},
"incoming_supply": {
"amount": "0",
"denom": "bnb"
},
"outgoing_supply": {
"amount": "0",
"denom": "bnb"
},
"time_elapsed": "0s",
"time_limited_current_supply": {
"amount": "0",
"denom": "bnb"
}
}
]
}

144
x/bep3/module.go Normal file
View File

@ -0,0 +1,144 @@
package bep3
import (
"context"
"encoding/json"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/0glabs/0g-chain/x/bep3/client/cli"
"github.com/0glabs/0g-chain/x/bep3/keeper"
"github.com/0glabs/0g-chain/x/bep3/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic defines the basic application module used by the bep3 module.
type AppModuleBasic struct{}
// Name returns the bep3 module's name.
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// RegisterLegacyAminoCodec register module codec
func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
types.RegisterLegacyAminoCodec(cdc)
}
// DefaultGenesis returns default genesis state as raw bytes for the bep3
// module.
func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
gs := types.DefaultGenesisState()
return cdc.MustMarshalJSON(&gs)
}
// ValidateGenesis performs genesis state validation for the bep3 module.
func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error {
var gs types.GenesisState
err := cdc.UnmarshalJSON(bz, &gs)
if err != nil {
return err
}
return gs.Validate()
}
// RegisterInterfaces implements InterfaceModule.RegisterInterfaces
func (a AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) {
types.RegisterInterfaces(registry)
}
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the gov module.
func (a AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {
types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx))
}
// ConsensusVersion implements AppModule/ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 {
return 1
}
// GetTxCmd returns the root tx command for the bep3 module.
func (AppModuleBasic) GetTxCmd() *cobra.Command {
return cli.GetTxCmd()
}
// GetQueryCmd returns no root query command for the bep3 module.
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
return cli.GetQueryCmd(types.StoreKey)
}
//____________________________________________________________________________
// AppModule implements the sdk.AppModule interface.
type AppModule struct {
AppModuleBasic
keeper keeper.Keeper
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper keeper.Keeper, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
}
}
// Name returns the bep3 module's name.
func (AppModule) Name() string {
return types.ModuleName
}
// RegisterInvariants registers the bep3 module invariants.
func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// RegisterServices registers module services.
func (am AppModule) RegisterServices(cfg module.Configurator) {
types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
types.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServerImpl(am.keeper))
}
// InitGenesis performs genesis initialization for the bep3 module. It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.RawMessage) []abci.ValidatorUpdate {
var genState types.GenesisState
// Initialize global index to index in genesis state
cdc.MustUnmarshalJSON(gs, &genState)
InitGenesis(ctx, am.keeper, am.accountKeeper, &genState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis returns the exported genesis state as raw bytes for the bep3
// module.
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return cdc.MustMarshalJSON(&gs)
}
// BeginBlock returns the begin blocker for the bep3 module.
func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {
BeginBlocker(ctx, am.keeper)
}
// EndBlock returns the end blocker for the bep3 module. It returns no validator updates.
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

View File

@ -0,0 +1,40 @@
<!--
order: 1
-->
# Concepts
The BEP3 module implements the [BEP3 protocol](https://github.com/binance-chain/BEPs/blob/master/BEP3.md) for secure cross-chain asset transfers between Kava and other BEP3 compatible chains, such as Binance Chain. Transactions are witnessed and relayed between the two blockchains by Binance's BEP3 deputy process. The deputy maintains an address on both chains and is responsible for delivering tokens upon the successful completion of an Atomic Swap. Learn more about the BEP3 deputy process [here](https://github.com/binance-chain/bep3-deputy).
## Requirements
Kava
- The deputys Kava address on mainnet is **kava1r4v2zdhdalfj2ydazallqvrus9fkphmglhn6u6**.
- Kava's official API endpoint is https://kava3.data.kava.io.
Binance Chain
- The deputys Binance Chain address on mainnet is **bnb1jh7uv2rm6339yue8k4mj9406k3509kr4wt5nxn**.
- We recommend using https://testnet-dex.binance.org/ as Binance Chains API endpoint.
Kava's [JavaScript SDK](https://github.com/Kava-Labs/javascript-sdk) and Binance Chains [JavaScript SDK](https://github.com/binance-chain/javascript-sdk) can be used to create, claim, and refund swaps.
## Binance Chain to Kava
When a user wants to transfer tokens from Binance Chain to Kava, the following steps are taken:
1. Users tokens are locked on Binance Chain along with the hash of a secret only known to the user. If the secret is not revealed before the deadline, the tokens are refundable.
2. The deputy sends a message to Kava saying “a user has locked X tokens, if their secret is revealed before the deadline issue them an equivalent amount of pegged tokens”.
3. The user reveals the secret on Kava and receives the pegged tokens.
4. The deputy relays the secret to Binance Chain and the original tokens are locked permanently.
![Binance Chain to Kava Diagram](./diagrams/BEP3_binance_chain_to_kava.jpg)
## Kava to Binance Chain
1. When a user wants to transfer tokens from Kava to Binance Chain by redeeming pegged tokens, the following steps are taken:
Users pegged tokens are locked on Kava along with the hash of a secret only known to the user. If the secret is not revealed before the deadline, the tokens are refundable.
2. The deputy sends a message to Binance Chain saying “a user has locked X pegged tokens, if their secret is revealed before the deadline issue them an equivalent amount of tokens”.
3. The user reveals the secret on Binance Chain and receives the tokens.
4. The deputy relays the secret to Kava and the pegged tokens are locked permanently.
![Kava to Binance Chain Diagram](./diagrams/BEP3_kava_to_binance_chain.jpg)

100
x/bep3/spec/02_state.md Normal file
View File

@ -0,0 +1,100 @@
<!--
order: 2
-->
# State
## Parameters and genesis state
`parameters` define the rules according to which swaps are executed. Parameter updates can be made via on-chain parameter update proposals.
```go
// Params governance parameters for bep3 module
type Params struct {
BnbDeputyAddress sdk.AccAddress `json:"bnb_deputy_address" yaml:"bnb_deputy_address"` // Bnbchain deputy address
BnbDeputyFixedFee sdkmath.Int `json:"bnb_deputy_fixed_fee" yaml:"bnb_deputy_fixed_fee"` // Deputy fixed fee in BNB
MinAmount sdkmath.Int `json:"min_amount" yaml:"min_amount"` // Minimum swap amount
MaxAmount sdkmath.Int `json:"max_amount" yaml:"max_amount"` // Maximum swap amount
MinBlockLock uint64 `json:"min_block_lock" yaml:"min_block_lock"` // Minimum swap block lock
MaxBlockLock uint64 `json:"max_block_lock" yaml:"max_block_lock"` // Maximum swap block lock
SupportedAssets AssetParams `json:"supported_assets" yaml:"supported_assets"` // Supported assets
}
// AssetParam governance parameters for each asset within a supported chain
type AssetParam struct {
Denom string `json:"denom" yaml:"denom"` // name of the asset
CoinID int `json:"coin_id" yaml:"coin_id"` // internationally recognized coin ID
Limit sdkmath.Int `json:"limit" yaml:"limit"` // asset supply limit
Active bool `json:"active" yaml:"active"` // denotes if asset is active or paused
}
```
`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the bep3 module to resume.
```go
// GenesisState - all bep3 state that must be provided at genesis
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"`
AssetSupplies AssetSupplies `json:"assets_supplies" yaml:"assets_supplies"`
}
```
## Types
AtomicSwap stores information about an individual atomic swap, including the sender, recipient, amount, random number hash (used to validate the secret and unlock funds), the status (open, completed, or expired). There are two types of atomic swaps:
- Incoming: assets are being sent to Kava from another blockchain.
- Outgoing: assets are being send to another blockchain from Kava.
```go
// AtomicSwap contains the information for an atomic swap
type AtomicSwap struct {
Amount sdk.Coins `json:"amount" yaml:"amount"`
RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
ExpireHeight int64 `json:"expire_height" yaml:"expire_height"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"`
SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
ClosedBlock int64 `json:"closed_block" yaml:"closed_block"`
Status SwapStatus `json:"status" yaml:"status"`
Direction SwapDirection `json:"direction" yaml:"direction"`
}
// SwapStatus is the status of an AtomicSwap
type SwapStatus byte
const (
NULL SwapStatus = 0x00
Open SwapStatus = 0x01
Completed SwapStatus = 0x02
Expired SwapStatus = 0x03
)
// SwapDirection is the direction of an AtomicSwap
type SwapDirection byte
const (
INVALID SwapDirection = 0x00
Incoming SwapDirection = 0x01
Outgoing SwapDirection = 0x02
)
```
AssetSupply stores information about an individual asset's BEP3 supply:
- Incoming supply: total amount in incoming swaps (being sent to the chain).
- Outgoing supply: total amount in outgoing swaps (being sent off the chain). It cannot be greater than the current supply.
- Current supply: the amount that the deputy has released - it is the active supply on Kava. It is equal to the total amount successfully claimed from incoming swaps minus the total amount claimed from outgoing swaps.
- Supply limit: the maximum amount currently allowed on Kava. The supply limit can be increased by Kava's stability committee, subject to an on-chain proposal vote.
```go
// AssetSupply contains information about an asset's supply
type AssetSupply struct {
Denom string `json:"denom" yaml:"denom"`
IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"`
OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"`
CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"`
SupplyLimit sdk.Coin `json:"supply_limit" yaml:"supply_limit"`
}
```

View File

@ -0,0 +1,48 @@
<!--
order: 3
-->
# Messages
## Create swap
Swaps are created using the `MsgCreateAtomicSwap` message type.
```go
// MsgCreateAtomicSwap contains an AtomicSwap struct
type MsgCreateAtomicSwap struct {
From sdk.AccAddress `json:"from" yaml:"from"`
To sdk.AccAddress `json:"to" yaml:"to"`
RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
Amount sdk.Coins `json:"amount" yaml:"amount"`
HeightSpan int64 `json:"height_span" yaml:"height_span"`
}
```
## Claim swap
Active swaps are claimed using the `MsgClaimAtomicSwap` message type.
```go
// MsgClaimAtomicSwap defines a AtomicSwap claim
type MsgClaimAtomicSwap struct {
From sdk.AccAddress `json:"from" yaml:"from"`
SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
RandomNumber tmbytes.HexBytes `json:"random_number" yaml:"random_number"`
}
```
## Refund swap
Expired swaps are refunded using the `MsgRefundAtomicSwap` message type.
```go
// MsgRefundAtomicSwap defines a refund msg
type MsgRefundAtomicSwap struct {
From sdk.AccAddress `json:"from" yaml:"from"`
SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"`
}
```

55
x/bep3/spec/04_events.md Normal file
View File

@ -0,0 +1,55 @@
<!--
order: 4
-->
# Events
The `x/bep3` module emits the following events:
## Handlers
### MsgCreateAtomicSwap
| Type | Attribute Key | Attribute Value |
|--------------------|--------------------|---------------------------|
| create_atomic_swap | sender | `{sender address}` |
| create_atomic_swap | recipient | `{recipient address}` |
| create_atomic_swap | atomic_swap_id | `{swap ID}` |
| create_atomic_swap | random_number_hash | `{random number hash}` |
| create_atomic_swap | timestamp | `{timestamp}` |
| create_atomic_swap | sender_other_chain | `{sender other chain}` |
| create_atomic_swap | expire_height | `{swap expiration block}` |
| create_atomic_swap | amount | `{coin amount}` |
| create_atomic_swap | direction | `{incoming or outgoing}` |
| message | module | bep3 |
| message | sender | `{sender address}` |
### MsgClaimAtomicSwap
| Type | Attribute Key | Attribute Value |
|--------------------|--------------------|---------------------------|
| claim_atomic_swap | claim_sender | `{sender address}` |
| claim_atomic_swap | recipient | `{recipient address}` |
| claim_atomic_swap | atomic_swap_id | `{swap ID}` |
| claim_atomic_swap | random_number_hash | `{random number hash}` |
| claim_atomic_swap | random_number | `{secret random number}` |
| message | module | bep3 |
| message | sender | `{sender address}` |
## MsgRefundAtomicSwap
| Type | Attribute Key | Attribute Value |
|--------------------|--------------------|---------------------------|
| refund_atomic_swap | refund_sender | `{sender address}` |
| refund_atomic_swap | sender | `{swap creator address}` |
| refund_atomic_swap | atomic_swap_id | `{swap ID}` |
| refund_atomic_swap | random_number_hash | `{random number hash}` |
| message | module | bep3 |
| message | sender | `{sender address}` |
## BeginBlock
| Type | Attribute Key | Attribute Value |
|---------------|------------------|----------------------------------|
| swaps_expired | atomic_swap_ids | `{array of swap IDs}` |
| swaps_expired | expiration_block | `{block height at expiration}` |

26
x/bep3/spec/05_params.md Normal file
View File

@ -0,0 +1,26 @@
<!--
order: 5
-->
# Parameters
The bep3 module contains the following parameters:
| Key | Type | Example | Description |
| ----------------- | -------------- | --------------------------------------------- | -------------------------- |
| BnbDeputyAddress | sdk.AccAddress | "kava1r4v2zdhdalfj2ydazallqvrus9fkphmglhn6u6" | deputy's Kava address |
| BnbDeputyFixedFee | sdkmath.Int | sdkmath.NewInt(1000) | deputy's fixed bnb fee |
| MinAmount | sdkmath.Int | sdkmath.NewInt(0) | minimum swap amount |
| MaxAmount | sdkmath.Int | sdkmath.NewInt(1000000000000) | maximum swap amount |
| MinBlockLock | uint64 | 220 | minimum swap expire height |
| MaxBlockLock | uint64 | 270 | maximum swap expire height |
| SupportedAssets | AssetParams | []AssetParam | array of supported assets |
Each AssetParam has the following parameters:
| Key | Type | Example | Description |
| ----------------- | ----------- | ------------------- | ----------------------------- |
| AssetParam.Denom | string | "bnb" | asset's name |
| AssetParam.CoinID | int64 | 714 | asset's international coin ID |
| AssetParam.Limit | sdkmath.Int | sdkmath.NewInt(100) | asset's supply limit |
| AssetParam.Active | boolean | true | asset's state: live or paused |

View File

@ -0,0 +1,50 @@
<!--
order: 6
-->
# Begin Block
At the start of each block, atomic swaps that meet certain criteria are expired or deleted.
```go
func BeginBlocker(ctx sdk.Context, k Keeper) {
k.UpdateExpiredAtomicSwaps(ctx)
k.DeleteClosedAtomicSwapsFromLongtermStorage(ctx)
}
```
## Expiration
If an atomic swap's `ExpireHeight` is greater than the current block height, it will be expired. The logic to expire atomic swaps is as follows:
```go
var expiredSwapIDs []string
k.IterateAtomicSwapsByBlock(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool {
atomicSwap, found := k.GetAtomicSwap(ctx, id)
if !found {
return false
}
// Expire the uncompleted swap and update both indexes
atomicSwap.Status = types.Expired
k.RemoveFromByBlockIndex(ctx, atomicSwap)
k.SetAtomicSwap(ctx, atomicSwap)
expiredSwapIDs = append(expiredSwapIDs, hex.EncodeToString(atomicSwap.GetSwapID()))
return false
})
```
## Deletion
Atomic swaps are deleted 86400 blocks (one week, assuming a block time of 7 seconds) after being completed. The logic to delete atomic swaps is as follows:
```go
k.IterateAtomicSwapsLongtermStorage(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool {
swap, found := k.GetAtomicSwap(ctx, id)
if !found {
return false
}
k.RemoveAtomicSwap(ctx, swap.GetSwapID())
k.RemoveFromLongtermStorage(ctx, swap)
return false
})
```

27
x/bep3/spec/README.md Normal file
View File

@ -0,0 +1,27 @@
<!--
order: 0
title: "BEP3 Overview"
parent:
title: "bep3"
-->
# `bep3`
<!-- TOC -->
1. **[Concepts](01_concepts.md)**
2. **[State](02_state.md)**
3. **[Messages](03_messages.md)**
4. **[Events](04_events.md)**
5. **[Params](05_params.md)**
6. **[BeginBlock](06_begin_block.md)**
## Abstract
`x/bep3` is a module that handles cross-chain atomic swaps between Kava and blockchains that implement the BEP3 protocol. Atomic swaps are created, then either claimed before their expiration block or refunded after they've expired.
Several user interfaces support Kava BEP3 swaps:
- [Trust Wallet](https://trustwallet.com/)
- [Cosmostation](https://wallet.cosmostation.io/?network=kava)
- [Frontier Wallet](https://frontierwallet.com/)
Swaps can also be created, claimed, and refunded using Kava's [Javascript SDK](https://github.com/Kava-Labs/javascript-sdk) or CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

2411
x/bep3/types/bep3.pb.go Normal file

File diff suppressed because it is too large Load Diff

49
x/bep3/types/codec.go Normal file
View File

@ -0,0 +1,49 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
authzcodec "github.com/cosmos/cosmos-sdk/x/authz/codec"
)
// RegisterLegacyAminoCodec registers all the necessary types and interfaces for the
// bep3 module.
func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgCreateAtomicSwap{}, "bep3/MsgCreateAtomicSwap", nil)
cdc.RegisterConcrete(&MsgRefundAtomicSwap{}, "bep3/MsgRefundAtomicSwap", nil)
cdc.RegisterConcrete(&MsgClaimAtomicSwap{}, "bep3/MsgClaimAtomicSwap", nil)
}
func RegisterInterfaces(registry types.InterfaceRegistry) {
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgCreateAtomicSwap{},
&MsgRefundAtomicSwap{},
&MsgClaimAtomicSwap{},
)
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}
var (
amino = codec.NewLegacyAmino()
// ModuleCdc references the global x/bep3 module codec. Note, the codec should
// ONLY be used in certain instances of tests and for JSON encoding as Amino is
// still used for that purpose.
//
// The actual codec used for serialization should be provided to x/bep3 and
// defined at the application level.
ModuleCdc = codec.NewAminoCodec(amino)
)
func init() {
RegisterLegacyAminoCodec(amino)
cryptocodec.RegisterCrypto(amino)
// Register all Amino interfaces and concrete types on the authz Amino codec so that this can later be
// used to properly serialize MsgGrant and MsgExec instances
RegisterLegacyAminoCodec(authzcodec.Amino)
}

View File

@ -0,0 +1,38 @@
package types_test
import (
"time"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
tmtime "github.com/cometbft/cometbft/types/time"
"github.com/0glabs/0g-chain/x/bep3/types"
)
func i(in int64) sdkmath.Int { return sdkmath.NewInt(in) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() }
func atomicSwaps(count int) types.AtomicSwaps {
var swaps types.AtomicSwaps
for i := 0; i < count; i++ {
swap := atomicSwap(i)
swaps = append(swaps, swap)
}
return swaps
}
func atomicSwap(index int) types.AtomicSwap {
expireOffset := uint64((index * 15) + 360) // Default expire height + offet to match timestamp
timestamp := ts(index) // One minute apart
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
swap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, expireOffset, timestamp, zgAddrs[0],
zgAddrs[1], binanceAddrs[0].String(), binanceAddrs[1].String(), 1, types.SWAP_STATUS_OPEN, true, types.SWAP_DIRECTION_INCOMING)
return swap
}

46
x/bep3/types/errors.go Normal file
View File

@ -0,0 +1,46 @@
package types
import errorsmod "cosmossdk.io/errors"
// DONTCOVER
var (
// ErrInvalidTimestamp error for when an timestamp is outside of bounds. Assumes block time of 10 seconds.
ErrInvalidTimestamp = errorsmod.Register(ModuleName, 2, "timestamp can neither be 15 minutes ahead of the current time, nor 30 minutes later")
// ErrInvalidHeightSpan error for when a proposed height span is outside of lock time range
ErrInvalidHeightSpan = errorsmod.Register(ModuleName, 3, "height span is outside acceptable range")
// ErrInsufficientAmount error for when a swap's amount cannot cover the deputy's fixed fee
ErrInsufficientAmount = errorsmod.Register(ModuleName, 4, "amount cannot cover the deputy fixed fee")
// ErrAssetNotSupported error for when an asset is not supported
ErrAssetNotSupported = errorsmod.Register(ModuleName, 5, "asset not found")
// ErrAssetNotActive error for when an asset is currently inactive
ErrAssetNotActive = errorsmod.Register(ModuleName, 6, "asset is currently inactive")
// ErrAssetSupplyNotFound error for when an asset's supply is not found in the store
ErrAssetSupplyNotFound = errorsmod.Register(ModuleName, 7, "asset supply not found in store")
// ErrExceedsSupplyLimit error for when the proposed supply increase would put the supply above limit
ErrExceedsSupplyLimit = errorsmod.Register(ModuleName, 8, "asset supply over limit")
// ErrExceedsAvailableSupply error for when the proposed outgoing amount exceeds the total available supply
ErrExceedsAvailableSupply = errorsmod.Register(ModuleName, 9, "outgoing swap exceeds total available supply")
// ErrInvalidCurrentSupply error for when the proposed decrease would result in a negative current supplyx
ErrInvalidCurrentSupply = errorsmod.Register(ModuleName, 10, "supply decrease puts current asset supply below 0")
// ErrInvalidIncomingSupply error for when the proposed decrease would result in a negative incoming supply
ErrInvalidIncomingSupply = errorsmod.Register(ModuleName, 11, "supply decrease puts incoming asset supply below 0")
// ErrInvalidOutgoingSupply error for when the proposed decrease would result in a negative outgoing supply
ErrInvalidOutgoingSupply = errorsmod.Register(ModuleName, 12, "supply decrease puts outgoing asset supply below 0")
// ErrInvalidClaimSecret error when a submitted secret doesn't match an AtomicSwap's swapID
ErrInvalidClaimSecret = errorsmod.Register(ModuleName, 13, "hashed claim attempt does not match")
// ErrAtomicSwapAlreadyExists error for when an AtomicSwap with this swapID already exists
ErrAtomicSwapAlreadyExists = errorsmod.Register(ModuleName, 14, "atomic swap already exists")
// ErrAtomicSwapNotFound error for when an atomic swap is not found
ErrAtomicSwapNotFound = errorsmod.Register(ModuleName, 15, "atomic swap not found")
// ErrSwapNotRefundable error for when an AtomicSwap has not expired and cannot be refunded
ErrSwapNotRefundable = errorsmod.Register(ModuleName, 16, "atomic swap is still active and cannot be refunded")
// ErrSwapNotClaimable error for when an atomic swap is not open and cannot be claimed
ErrSwapNotClaimable = errorsmod.Register(ModuleName, 17, "atomic swap is not claimable")
// ErrInvalidAmount error for when a swap's amount is outside acceptable range
ErrInvalidAmount = errorsmod.Register(ModuleName, 18, "amount is outside acceptable range")
// ErrInvalidSwapAccount error for when a swap involves an invalid account
ErrInvalidSwapAccount = errorsmod.Register(ModuleName, 19, "atomic swap has invalid account")
// ErrExceedsTimeBasedSupplyLimit error for when the proposed supply increase would put the supply above limit for the current time period
ErrExceedsTimeBasedSupplyLimit = errorsmod.Register(ModuleName, 20, "asset supply over limit for current time period")
)

25
x/bep3/types/events.go Normal file
View File

@ -0,0 +1,25 @@
package types
// Events for bep3 module
const (
EventTypeCreateAtomicSwap = "create_atomic_swap"
EventTypeClaimAtomicSwap = "claim_atomic_swap"
EventTypeRefundAtomicSwap = "refund_atomic_swap"
EventTypeSwapsExpired = "swaps_expired"
AttributeValueCategory = ModuleName
AttributeKeySender = "sender"
AttributeKeyRecipient = "recipient"
AttributeKeyAtomicSwapID = "atomic_swap_id"
AttributeKeyRandomNumberHash = "random_number_hash"
AttributeKeyTimestamp = "timestamp"
AttributeKeySenderOtherChain = "sender_other_chain"
AttributeKeyExpireHeight = "expire_height"
AttributeKeyAmount = "amount"
AttributeKeyDirection = "direction"
AttributeKeyClaimSender = "claim_sender"
AttributeKeyRandomNumber = "random_number"
AttributeKeyRefundSender = "refund_sender"
AttributeKeyAtomicSwapIDs = "atomic_swap_ids"
AttributeExpirationBlock = "expiration_block"
)

View File

@ -0,0 +1,26 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
// BankKeeper defines the expected interface needed to retrieve account balances.
type BankKeeper interface {
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
}
// AccountKeeper defines the expected account keeper
type AccountKeeper interface {
GetModuleAddress(name string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, moduleName string) authtypes.ModuleAccountI
GetModuleAddressAndPermissions(moduleName string) (sdk.AccAddress, []string)
SetModuleAccount(ctx sdk.Context, macc authtypes.ModuleAccountI)
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI
NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI
SetAccount(ctx sdk.Context, acc authtypes.AccountI)
}

59
x/bep3/types/genesis.go Normal file
View File

@ -0,0 +1,59 @@
package types
import (
"encoding/hex"
"fmt"
"time"
)
// NewGenesisState creates a new GenesisState object
func NewGenesisState(params Params, swaps AtomicSwaps, supplies AssetSupplies, previousBlockTime time.Time) GenesisState {
return GenesisState{
Params: params,
AtomicSwaps: swaps,
Supplies: supplies,
PreviousBlockTime: previousBlockTime,
}
}
// DefaultGenesisState - default GenesisState used by Cosmos Hub
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultParams(),
AtomicSwaps{},
AssetSupplies{},
DefaultPreviousBlockTime,
)
}
// Validate validates genesis inputs. It returns error if validation of any input fails.
func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
ids := map[string]bool{}
for _, swap := range gs.AtomicSwaps {
if ids[hex.EncodeToString(swap.GetSwapID())] {
return fmt.Errorf("found duplicate atomic swap ID %s", hex.EncodeToString(swap.GetSwapID()))
}
if err := swap.Validate(); err != nil {
return err
}
ids[hex.EncodeToString(swap.GetSwapID())] = true
}
supplyDenoms := map[string]bool{}
for _, supply := range gs.Supplies {
if err := supply.Validate(); err != nil {
return err
}
if supplyDenoms[supply.GetDenom()] {
return fmt.Errorf("found duplicate denom in asset supplies %s", supply.GetDenom())
}
supplyDenoms[supply.GetDenom()] = true
}
return nil
}

514
x/bep3/types/genesis.pb.go Normal file
View File

@ -0,0 +1,514 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: zgc/bep3/v1beta1/genesis.proto
package types
import (
fmt "fmt"
_ "github.com/cosmos/gogoproto/gogoproto"
proto "github.com/cosmos/gogoproto/proto"
github_com_cosmos_gogoproto_types "github.com/cosmos/gogoproto/types"
_ "google.golang.org/protobuf/types/known/timestamppb"
io "io"
math "math"
math_bits "math/bits"
time "time"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
var _ = time.Kitchen
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// GenesisState defines the pricefeed module's genesis state.
type GenesisState struct {
// params defines all the parameters of the module.
Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"`
// atomic_swaps represents the state of stored atomic swaps
AtomicSwaps AtomicSwaps `protobuf:"bytes,2,rep,name=atomic_swaps,json=atomicSwaps,proto3,castrepeated=AtomicSwaps" json:"atomic_swaps"`
// supplies represents the supply information of each atomic swap
Supplies AssetSupplies `protobuf:"bytes,3,rep,name=supplies,proto3,castrepeated=AssetSupplies" json:"supplies"`
// previous_block_time represents the time of the previous block
PreviousBlockTime time.Time `protobuf:"bytes,4,opt,name=previous_block_time,json=previousBlockTime,proto3,stdtime" json:"previous_block_time"`
}
func (m *GenesisState) Reset() { *m = GenesisState{} }
func (m *GenesisState) String() string { return proto.CompactTextString(m) }
func (*GenesisState) ProtoMessage() {}
func (*GenesisState) Descriptor() ([]byte, []int) {
return fileDescriptor_887bb27f177aae40, []int{0}
}
func (m *GenesisState) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *GenesisState) XXX_Merge(src proto.Message) {
xxx_messageInfo_GenesisState.Merge(m, src)
}
func (m *GenesisState) XXX_Size() int {
return m.Size()
}
func (m *GenesisState) XXX_DiscardUnknown() {
xxx_messageInfo_GenesisState.DiscardUnknown(m)
}
var xxx_messageInfo_GenesisState proto.InternalMessageInfo
func (m *GenesisState) GetParams() Params {
if m != nil {
return m.Params
}
return Params{}
}
func (m *GenesisState) GetAtomicSwaps() AtomicSwaps {
if m != nil {
return m.AtomicSwaps
}
return nil
}
func (m *GenesisState) GetSupplies() AssetSupplies {
if m != nil {
return m.Supplies
}
return nil
}
func (m *GenesisState) GetPreviousBlockTime() time.Time {
if m != nil {
return m.PreviousBlockTime
}
return time.Time{}
}
func init() {
proto.RegisterType((*GenesisState)(nil), "zgc.bep3.v1beta1.GenesisState")
}
func init() { proto.RegisterFile("zgc/bep3/v1beta1/genesis.proto", fileDescriptor_887bb27f177aae40) }
var fileDescriptor_887bb27f177aae40 = []byte{
// 363 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x91, 0xcd, 0x6e, 0xe2, 0x30,
0x14, 0x85, 0x13, 0x40, 0x08, 0x25, 0x8c, 0x34, 0x13, 0x66, 0xa4, 0x88, 0x99, 0x49, 0x50, 0x37,
0x65, 0x53, 0x9b, 0x1f, 0xa9, 0x7b, 0xb2, 0xe9, 0x16, 0x05, 0x56, 0xdd, 0x20, 0x27, 0x72, 0x8d,
0xd5, 0x04, 0x5b, 0xd8, 0x81, 0xc2, 0x53, 0xf0, 0x1c, 0x7d, 0x8e, 0x2e, 0x58, 0xb2, 0xec, 0xaa,
0x54, 0xf0, 0x22, 0x95, 0x9d, 0x50, 0xa4, 0xd2, 0x9d, 0xef, 0x3d, 0xe7, 0x7e, 0xd7, 0x3e, 0xb6,
0xbc, 0x35, 0x89, 0x61, 0x84, 0x79, 0x1f, 0x2e, 0xba, 0x11, 0x96, 0xa8, 0x0b, 0x09, 0x9e, 0x61,
0x41, 0x05, 0xe0, 0x73, 0x26, 0x99, 0xf3, 0x73, 0x4d, 0x62, 0xa0, 0x74, 0x50, 0xe8, 0xcd, 0xdf,
0x84, 0x11, 0xa6, 0x45, 0xa8, 0x4e, 0xb9, 0xaf, 0xe9, 0x13, 0xc6, 0x48, 0x82, 0xa1, 0xae, 0xa2,
0xec, 0x01, 0x4a, 0x9a, 0x62, 0x21, 0x51, 0xca, 0x0b, 0xc3, 0xdf, 0x8b, 0x45, 0x9a, 0xaa, 0xc5,
0xab, 0x97, 0x92, 0x55, 0xbf, 0xcb, 0xf7, 0x8e, 0x24, 0x92, 0xd8, 0xb9, 0xb5, 0xaa, 0x1c, 0xcd,
0x51, 0x2a, 0x5c, 0xb3, 0x65, 0xb6, 0xed, 0x9e, 0x0b, 0xbe, 0xde, 0x03, 0x0c, 0xb5, 0x1e, 0x54,
0xb6, 0x6f, 0xbe, 0x11, 0x16, 0x6e, 0x67, 0x6c, 0xd5, 0x91, 0x64, 0x29, 0x8d, 0x27, 0x62, 0x89,
0xb8, 0x70, 0x4b, 0xad, 0x72, 0xdb, 0xee, 0xfd, 0xbb, 0x9c, 0x1e, 0x68, 0xd7, 0x68, 0x89, 0x78,
0xd0, 0x50, 0x84, 0xe7, 0xbd, 0x6f, 0x9f, 0x7b, 0x22, 0xb4, 0xd1, 0xb9, 0x70, 0x86, 0x56, 0x4d,
0x64, 0x9c, 0x27, 0x14, 0x0b, 0xb7, 0xac, 0x89, 0xff, 0xbf, 0x21, 0x0a, 0x81, 0xe5, 0x48, 0xd9,
0x56, 0xc1, 0x9f, 0x02, 0xf9, 0xe3, 0xdc, 0xa4, 0x58, 0x84, 0x9f, 0x14, 0x67, 0x6c, 0x35, 0xf8,
0x1c, 0x2f, 0x28, 0xcb, 0xc4, 0x24, 0x4a, 0x58, 0xfc, 0x38, 0x51, 0x79, 0xb9, 0x15, 0xfd, 0xd8,
0x26, 0xc8, 0xc3, 0x04, 0xa7, 0x30, 0xc1, 0xf8, 0x14, 0x66, 0x50, 0x53, 0xe4, 0xcd, 0xde, 0x37,
0xc3, 0x5f, 0x27, 0x40, 0xa0, 0xe6, 0x95, 0x23, 0x18, 0x6c, 0x0f, 0x9e, 0xb9, 0x3b, 0x78, 0xe6,
0xfb, 0xc1, 0x33, 0x37, 0x47, 0xcf, 0xd8, 0x1d, 0x3d, 0xe3, 0xf5, 0xe8, 0x19, 0xf7, 0xd7, 0x84,
0xca, 0x69, 0x16, 0x81, 0x98, 0xa5, 0xb0, 0x43, 0x12, 0x14, 0x09, 0xd8, 0x21, 0x37, 0xf1, 0x14,
0xd1, 0x19, 0x7c, 0xca, 0xbf, 0x45, 0xae, 0x38, 0x16, 0x51, 0x55, 0xef, 0xec, 0x7f, 0x04, 0x00,
0x00, 0xff, 0xff, 0xa9, 0xa5, 0x69, 0xc0, 0x18, 0x02, 0x00, 0x00,
}
func (m *GenesisState) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
n1, err1 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.PreviousBlockTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.PreviousBlockTime):])
if err1 != nil {
return 0, err1
}
i -= n1
i = encodeVarintGenesis(dAtA, i, uint64(n1))
i--
dAtA[i] = 0x22
if len(m.Supplies) > 0 {
for iNdEx := len(m.Supplies) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Supplies[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintGenesis(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x1a
}
}
if len(m.AtomicSwaps) > 0 {
for iNdEx := len(m.AtomicSwaps) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.AtomicSwaps[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintGenesis(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x12
}
}
{
size, err := m.Params.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintGenesis(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0xa
return len(dAtA) - i, nil
}
func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int {
offset -= sovGenesis(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return base
}
func (m *GenesisState) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = m.Params.Size()
n += 1 + l + sovGenesis(uint64(l))
if len(m.AtomicSwaps) > 0 {
for _, e := range m.AtomicSwaps {
l = e.Size()
n += 1 + l + sovGenesis(uint64(l))
}
}
if len(m.Supplies) > 0 {
for _, e := range m.Supplies {
l = e.Size()
n += 1 + l + sovGenesis(uint64(l))
}
}
l = github_com_cosmos_gogoproto_types.SizeOfStdTime(m.PreviousBlockTime)
n += 1 + l + sovGenesis(uint64(l))
return n
}
func sovGenesis(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
func sozGenesis(x uint64) (n int) {
return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *GenesisState) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: GenesisState: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenesis
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthGenesis
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field AtomicSwaps", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenesis
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthGenesis
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.AtomicSwaps = append(m.AtomicSwaps, AtomicSwap{})
if err := m.AtomicSwaps[len(m.AtomicSwaps)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Supplies", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenesis
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthGenesis
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Supplies = append(m.Supplies, AssetSupply{})
if err := m.Supplies[len(m.Supplies)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field PreviousBlockTime", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenesis
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthGenesis
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(&m.PreviousBlockTime, dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenesis(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthGenesis
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipGenesis(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenesis
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenesis
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
case 1:
iNdEx += 8
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenesis
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if length < 0 {
return 0, ErrInvalidLengthGenesis
}
iNdEx += length
case 3:
depth++
case 4:
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupGenesis
}
depth--
case 5:
iNdEx += 4
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthGenesis
}
if depth == 0 {
return iNdEx, nil
}
}
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group")
)

View File

@ -0,0 +1,123 @@
package types_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type GenesisTestSuite struct {
suite.Suite
swaps types.AtomicSwaps
supplies types.AssetSupplies
}
func (suite *GenesisTestSuite) SetupTest() {
coin := sdk.NewCoin("a0gi", sdk.OneInt())
suite.swaps = atomicSwaps(10)
supply := types.NewAssetSupply(coin, coin, coin, coin, time.Duration(0))
suite.supplies = types.AssetSupplies{supply}
}
func (suite *GenesisTestSuite) TestValidate() {
type args struct {
swaps types.AtomicSwaps
supplies types.AssetSupplies
previousBlockTime time.Time
}
testCases := []struct {
name string
args args
expectPass bool
}{
{
"default",
args{
swaps: types.AtomicSwaps{},
previousBlockTime: types.DefaultPreviousBlockTime,
},
true,
},
{
"with swaps",
args{
swaps: suite.swaps,
previousBlockTime: types.DefaultPreviousBlockTime,
},
true,
},
{
"with supplies",
args{
swaps: types.AtomicSwaps{},
supplies: suite.supplies,
previousBlockTime: types.DefaultPreviousBlockTime,
},
true,
},
{
"invalid supply",
args{
swaps: types.AtomicSwaps{},
supplies: types.AssetSupplies{{IncomingSupply: sdk.Coin{Denom: "Invalid", Amount: sdk.ZeroInt()}}},
previousBlockTime: types.DefaultPreviousBlockTime,
},
false,
},
{
"duplicate swaps",
args{
swaps: types.AtomicSwaps{suite.swaps[2], suite.swaps[2]},
previousBlockTime: types.DefaultPreviousBlockTime,
},
false,
},
{
"invalid swap",
args{
swaps: types.AtomicSwaps{{Amount: sdk.Coins{sdk.Coin{Denom: "Invalid Denom", Amount: sdkmath.NewInt(-1)}}}},
previousBlockTime: types.DefaultPreviousBlockTime,
},
false,
},
{
"blocktime not set",
args{
swaps: types.AtomicSwaps{},
},
true,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
var gs types.GenesisState
if tc.name == "default" {
gs = types.DefaultGenesisState()
} else {
gs = types.NewGenesisState(types.DefaultParams(), tc.args.swaps, tc.args.supplies, tc.args.previousBlockTime)
}
err := gs.Validate()
if tc.expectPass {
suite.Require().NoError(err, tc.name)
} else {
suite.Require().Error(err, tc.name)
}
})
}
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

36
x/bep3/types/hash.go Normal file
View File

@ -0,0 +1,36 @@
package types
import (
"crypto/rand"
"encoding/binary"
"strings"
"github.com/cometbft/cometbft/crypto/tmhash"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GenerateSecureRandomNumber generates cryptographically strong pseudo-random number
func GenerateSecureRandomNumber() ([]byte, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return []byte{}, err
}
return bytes, nil
}
// CalculateRandomHash calculates the hash of a number and timestamp
func CalculateRandomHash(randomNumber []byte, timestamp int64) []byte {
data := make([]byte, RandomNumberLength+Int64Size)
copy(data[:RandomNumberLength], randomNumber)
binary.BigEndian.PutUint64(data[RandomNumberLength:], uint64(timestamp))
return tmhash.Sum(data)
}
// CalculateSwapID calculates the hash of a RandomNumberHash, sdk.AccAddress, and string
func CalculateSwapID(randomNumberHash []byte, sender sdk.AccAddress, senderOtherChain string) []byte {
senderOtherChain = strings.ToLower(senderOtherChain)
data := randomNumberHash
data = append(data, sender.Bytes()...)
data = append(data, []byte(senderOtherChain)...)
return tmhash.Sum(data)
}

61
x/bep3/types/hash_test.go Normal file
View File

@ -0,0 +1,61 @@
package types_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type HashTestSuite struct {
suite.Suite
addrs []sdk.AccAddress
timestamps []int64
}
func (suite *HashTestSuite) SetupTest() {
// Generate 10 addresses
_, addrs := app.GeneratePrivKeyAddressPairs(10)
// Generate 10 timestamps
var timestamps []int64
for i := 0; i < 10; i++ {
timestamps = append(timestamps, ts(i))
}
suite.addrs = addrs
suite.timestamps = timestamps
}
func (suite *HashTestSuite) TestGenerateSecureRandomNumber() {
secureRandomNumber, err := types.GenerateSecureRandomNumber()
suite.Nil(err)
suite.NotNil(secureRandomNumber)
suite.Equal(32, len(secureRandomNumber))
}
func (suite *HashTestSuite) TestCalculateRandomHash() {
randomNumber, _ := types.GenerateSecureRandomNumber()
hash := types.CalculateRandomHash(randomNumber[:], suite.timestamps[0])
suite.NotNil(hash)
suite.Equal(32, len(hash))
}
func (suite *HashTestSuite) TestCalculateSwapID() {
randomNumber, _ := types.GenerateSecureRandomNumber()
hash := types.CalculateRandomHash(randomNumber[:], suite.timestamps[3])
swapID := types.CalculateSwapID(hash, suite.addrs[3], suite.addrs[5].String())
suite.NotNil(swapID)
suite.Equal(32, len(swapID))
diffHash := types.CalculateRandomHash(randomNumber[:], suite.timestamps[2])
diffSwapID := types.CalculateSwapID(diffHash, suite.addrs[3], suite.addrs[5].String())
suite.NotEqual(swapID, diffSwapID)
}
func TestHashTestSuite(t *testing.T) {
suite.Run(t, new(HashTestSuite))
}

36
x/bep3/types/keys.go Normal file
View File

@ -0,0 +1,36 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName is the name of the module
ModuleName = "bep3"
// StoreKey to be used when creating the KVStore
StoreKey = ModuleName
// RouterKey to be used for routing msgs
RouterKey = ModuleName
// DefaultParamspace default namestore
DefaultParamspace = ModuleName
// DefaultLongtermStorageDuration is 1 week (assuming a block time of 7 seconds)
DefaultLongtermStorageDuration uint64 = 86400
)
// Key prefixes
var (
AtomicSwapKeyPrefix = []byte{0x00} // prefix for keys that store AtomicSwaps
AtomicSwapByBlockPrefix = []byte{0x01} // prefix for keys of the AtomicSwapsByBlock index
AtomicSwapLongtermStoragePrefix = []byte{0x02} // prefix for keys of the AtomicSwapLongtermStorage index
AssetSupplyPrefix = []byte{0x03}
PreviousBlockTimeKey = []byte{0x04}
)
// GetAtomicSwapByHeightKey is used by the AtomicSwapByBlock index and AtomicSwapLongtermStorage index
func GetAtomicSwapByHeightKey(height uint64, swapID []byte) []byte {
return append(sdk.Uint64ToBigEndian(height), swapID...)
}

258
x/bep3/types/msg.go Normal file
View File

@ -0,0 +1,258 @@
package types
import (
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/cometbft/cometbft/crypto"
tmbytes "github.com/cometbft/cometbft/libs/bytes"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
const (
CreateAtomicSwap = "createAtomicSwap"
ClaimAtomicSwap = "claimAtomicSwap"
RefundAtomicSwap = "refundAtomicSwap"
CalcSwapID = "calcSwapID"
Int64Size = 8
RandomNumberHashLength = 32
RandomNumberLength = 32
MaxOtherChainAddrLength = 64
SwapIDLength = 32
MaxExpectedIncomeLength = 64
)
// ensure Msg interface compliance at compile time
var (
_ sdk.Msg = &MsgCreateAtomicSwap{}
_ sdk.Msg = &MsgClaimAtomicSwap{}
_ sdk.Msg = &MsgRefundAtomicSwap{}
AtomicSwapCoinsAccAddr = sdk.AccAddress(crypto.AddressHash([]byte("0gChainAtomicSwapCoins")))
)
// NewMsgCreateAtomicSwap initializes a new MsgCreateAtomicSwap
func NewMsgCreateAtomicSwap(from, to string, recipientOtherChain,
senderOtherChain string, randomNumberHash tmbytes.HexBytes, timestamp int64,
amount sdk.Coins, heightSpan uint64,
) MsgCreateAtomicSwap {
return MsgCreateAtomicSwap{
From: from,
To: to,
RecipientOtherChain: recipientOtherChain,
SenderOtherChain: senderOtherChain,
RandomNumberHash: randomNumberHash.String(),
Timestamp: timestamp,
Amount: amount,
HeightSpan: heightSpan,
}
}
// Route establishes the route for the MsgCreateAtomicSwap
func (msg MsgCreateAtomicSwap) Route() string { return RouterKey }
// Type is the name of MsgCreateAtomicSwap
func (msg MsgCreateAtomicSwap) Type() string { return CreateAtomicSwap }
// String prints the MsgCreateAtomicSwap
func (msg MsgCreateAtomicSwap) String() string {
return fmt.Sprintf("AtomicSwap{%v#%v#%v#%v#%v#%v#%v#%v}",
msg.From, msg.To, msg.RecipientOtherChain, msg.SenderOtherChain,
msg.RandomNumberHash, msg.Timestamp, msg.Amount, msg.HeightSpan)
}
// GetInvolvedAddresses gets the addresses involved in a MsgCreateAtomicSwap
func (msg MsgCreateAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress {
return append(msg.GetSigners(), AtomicSwapCoinsAccAddr)
}
// GetSigners gets the signers of a MsgCreateAtomicSwap
func (msg MsgCreateAtomicSwap) GetSigners() []sdk.AccAddress {
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
panic(err)
}
return []sdk.AccAddress{from}
}
// ValidateBasic validates the MsgCreateAtomicSwap
func (msg MsgCreateAtomicSwap) ValidateBasic() error {
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
to, err := sdk.AccAddressFromBech32(msg.To)
if err != nil {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
if from.Empty() {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if to.Empty() {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "recipient address cannot be empty")
}
if strings.TrimSpace(msg.RecipientOtherChain) == "" {
return errors.New("missing recipient address on other chain")
}
if len(msg.RecipientOtherChain) > MaxOtherChainAddrLength {
return fmt.Errorf("the length of recipient address on other chain should be less than %d", MaxOtherChainAddrLength)
}
if len(msg.SenderOtherChain) > MaxOtherChainAddrLength {
return fmt.Errorf("the length of sender address on other chain should be less than %d", MaxOtherChainAddrLength)
}
randomNumberHash, err := hex.DecodeString(msg.RandomNumberHash)
if err != nil {
return fmt.Errorf("random number hash should be valid hex: %v", err)
}
if len(randomNumberHash) != RandomNumberHashLength {
return fmt.Errorf("the length of random number hash should be %d", RandomNumberHashLength)
}
if msg.Timestamp <= 0 {
return errors.New("timestamp must be positive")
}
if len(msg.Amount) == 0 {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, "amount cannot be empty")
}
if !msg.Amount.IsValid() {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String())
}
if msg.HeightSpan <= 0 {
return errors.New("height span must be positive")
}
return nil
}
// GetSignBytes gets the sign bytes of a MsgCreateAtomicSwap
func (msg MsgCreateAtomicSwap) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}
// NewMsgClaimAtomicSwap initializes a new MsgClaimAtomicSwap
func NewMsgClaimAtomicSwap(from string, swapID, randomNumber tmbytes.HexBytes) MsgClaimAtomicSwap {
return MsgClaimAtomicSwap{
From: from,
SwapID: swapID.String(),
RandomNumber: randomNumber.String(),
}
}
// Route establishes the route for the MsgClaimAtomicSwap
func (msg MsgClaimAtomicSwap) Route() string { return RouterKey }
// Type is the name of MsgClaimAtomicSwap
func (msg MsgClaimAtomicSwap) Type() string { return ClaimAtomicSwap }
// String prints the MsgClaimAtomicSwap
func (msg MsgClaimAtomicSwap) String() string {
return fmt.Sprintf("claimAtomicSwap{%v#%v#%v}", msg.From, msg.SwapID, msg.RandomNumber)
}
// GetInvolvedAddresses gets the addresses involved in a MsgClaimAtomicSwap
func (msg MsgClaimAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress {
return append(msg.GetSigners(), AtomicSwapCoinsAccAddr)
}
// GetSigners gets the signers of a MsgClaimAtomicSwap
func (msg MsgClaimAtomicSwap) GetSigners() []sdk.AccAddress {
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
panic(err)
}
return []sdk.AccAddress{from}
}
// ValidateBasic validates the MsgClaimAtomicSwap
func (msg MsgClaimAtomicSwap) ValidateBasic() error {
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
if from.Empty() {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
swapID, err := hex.DecodeString(msg.SwapID)
if err != nil {
return fmt.Errorf("swap id should be valid hex: %v", err)
}
if len(swapID) != SwapIDLength {
return fmt.Errorf("the length of swapID should be %d", SwapIDLength)
}
randomNumber, err := hex.DecodeString(msg.RandomNumber)
if err != nil {
return fmt.Errorf("random number should be valid hex: %v", err)
}
if len(randomNumber) != RandomNumberLength {
return fmt.Errorf("the length of random number should be %d", RandomNumberLength)
}
return nil
}
// GetSignBytes gets the sign bytes of a MsgClaimAtomicSwap
func (msg MsgClaimAtomicSwap) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}
// NewMsgRefundAtomicSwap initializes a new MsgRefundAtomicSwap
func NewMsgRefundAtomicSwap(from string, swapID tmbytes.HexBytes) MsgRefundAtomicSwap {
return MsgRefundAtomicSwap{
From: from,
SwapID: swapID.String(),
}
}
// Route establishes the route for the MsgRefundAtomicSwap
func (msg MsgRefundAtomicSwap) Route() string { return RouterKey }
// Type is the name of MsgRefundAtomicSwap
func (msg MsgRefundAtomicSwap) Type() string { return RefundAtomicSwap }
// String prints the MsgRefundAtomicSwap
func (msg MsgRefundAtomicSwap) String() string {
return fmt.Sprintf("refundAtomicSwap{%v#%v}", msg.From, msg.SwapID)
}
// GetInvolvedAddresses gets the addresses involved in a MsgRefundAtomicSwap
func (msg MsgRefundAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress {
return append(msg.GetSigners(), AtomicSwapCoinsAccAddr)
}
// GetSigners gets the signers of a MsgRefundAtomicSwap
func (msg MsgRefundAtomicSwap) GetSigners() []sdk.AccAddress {
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
panic(err)
}
return []sdk.AccAddress{from}
}
// ValidateBasic validates the MsgRefundAtomicSwap
func (msg MsgRefundAtomicSwap) ValidateBasic() error {
from, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
if from.Empty() {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
swapID, err := hex.DecodeString(msg.SwapID)
if err != nil {
return fmt.Errorf("swap id should be valid hex: %v", err)
}
if len(swapID) != SwapIDLength {
return fmt.Errorf("the length of swapID should be %d", SwapIDLength)
}
return nil
}
// GetSignBytes gets the sign bytes of a MsgRefundAtomicSwap
func (msg MsgRefundAtomicSwap) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}

143
x/bep3/types/msg_test.go Normal file
View File

@ -0,0 +1,143 @@
package types_test
import (
"testing"
"github.com/cometbft/cometbft/crypto"
tmbytes "github.com/cometbft/cometbft/libs/bytes"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
var (
coinsSingle = sdk.NewCoins(sdk.NewInt64Coin("bnb", 50000))
binanceAddrs = []sdk.AccAddress{}
zgAddrs = []sdk.AccAddress{}
randomNumberBytes = []byte{15}
timestampInt64 = int64(100)
randomNumberHash = tmbytes.HexBytes(types.CalculateRandomHash(randomNumberBytes, timestampInt64))
)
func init() {
app.SetSDKConfig()
// Must be set after SetSDKConfig to use 0g Bech32 prefix instead of cosmos
binanceAddrs = []sdk.AccAddress{
sdk.AccAddress(crypto.AddressHash([]byte("BinanceTest1"))),
sdk.AccAddress(crypto.AddressHash([]byte("BinanceTest2"))),
}
zgAddrs = []sdk.AccAddress{
sdk.AccAddress(crypto.AddressHash([]byte("0gTest1"))),
sdk.AccAddress(crypto.AddressHash([]byte("0gTest2"))),
}
}
type MsgTestSuite struct {
suite.Suite
}
func (suite *MsgTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
}
func (suite *MsgTestSuite) TestMsgCreateAtomicSwap() {
tests := []struct {
description string
from sdk.AccAddress
to sdk.AccAddress
recipientOtherChain string
senderOtherChain string
randomNumberHash string
timestamp int64
amount sdk.Coins
heightSpan uint64
expectPass bool
}{
{"normal cross-chain", binanceAddrs[0], zgAddrs[0], zgAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash.String(), timestampInt64, coinsSingle, 500, true},
{"without other chain fields", binanceAddrs[0], zgAddrs[0], "", "", randomNumberHash.String(), timestampInt64, coinsSingle, 500, false},
{"invalid amount", binanceAddrs[0], zgAddrs[0], zgAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash.String(), timestampInt64, nil, 500, false},
{"invalid from address", sdk.AccAddress{}, zgAddrs[0], zgAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash.String(), timestampInt64, coinsSingle, 500, false},
{"invalid to address", binanceAddrs[0], sdk.AccAddress{}, zgAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash.String(), timestampInt64, coinsSingle, 500, false},
{"invalid rand hash", binanceAddrs[0], zgAddrs[0], zgAddrs[0].String(), binanceAddrs[0].String(), "ff", timestampInt64, coinsSingle, 500, false},
}
for i, tc := range tests {
msg := types.MsgCreateAtomicSwap{
tc.from.String(),
tc.to.String(),
tc.recipientOtherChain,
tc.senderOtherChain,
tc.randomNumberHash,
tc.timestamp,
tc.amount,
tc.heightSpan,
}
if tc.expectPass {
suite.NoError(msg.ValidateBasic(), "test: %v", i)
} else {
suite.Error(msg.ValidateBasic(), "test: %v", i)
}
}
}
func (suite *MsgTestSuite) TestMsgClaimAtomicSwap() {
swapID := types.CalculateSwapID(randomNumberHash, binanceAddrs[0], "")
tests := []struct {
description string
from sdk.AccAddress
swapID tmbytes.HexBytes
randomNumber tmbytes.HexBytes
expectPass bool
}{
{"normal", binanceAddrs[0], swapID, randomNumberHash, true},
{"invalid from address", sdk.AccAddress{}, swapID, randomNumberHash, false},
}
for i, tc := range tests {
msg := types.NewMsgClaimAtomicSwap(
tc.from.String(),
tc.swapID,
tc.randomNumber,
)
if tc.expectPass {
suite.NoError(msg.ValidateBasic(), "test: %v", i)
} else {
suite.Error(msg.ValidateBasic(), "test: %v", i)
}
}
}
func (suite *MsgTestSuite) TestMsgRefundAtomicSwap() {
swapID := types.CalculateSwapID(randomNumberHash, binanceAddrs[0], "")
tests := []struct {
description string
from sdk.AccAddress
swapID tmbytes.HexBytes
expectPass bool
}{
{"normal", binanceAddrs[0], swapID, true},
{"invalid from address", sdk.AccAddress{}, swapID, false},
}
for i, tc := range tests {
msg := types.NewMsgRefundAtomicSwap(
tc.from.String(),
tc.swapID,
)
if tc.expectPass {
suite.NoError(msg.ValidateBasic(), "test: %v", i)
} else {
suite.Error(msg.ValidateBasic(), "test: %v", i)
}
}
}
func TestMsgTestSuite(t *testing.T) {
suite.Run(t, new(MsgTestSuite))
}

150
x/bep3/types/params.go Normal file
View File

@ -0,0 +1,150 @@
package types
import (
"fmt"
"time"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
tmtime "github.com/cometbft/cometbft/types/time"
)
const (
bech32MainPrefix = "0g"
)
// Parameter keys
var (
KeyAssetParams = []byte("AssetParams")
DefaultBnbDeputyFixedFee sdkmath.Int = sdkmath.NewInt(1000) // 0.00001 BNB
DefaultMinAmount sdkmath.Int = sdk.ZeroInt()
DefaultMaxAmount sdkmath.Int = sdkmath.NewInt(1000000000000) // 10,000 BNB
DefaultMinBlockLock uint64 = 220
DefaultMaxBlockLock uint64 = 270
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(1, 0))
)
// NewParams returns a new params object
func NewParams(ap []AssetParam) Params {
return Params{
AssetParams: ap,
}
}
// DefaultParams returns default params for bep3 module
func DefaultParams() Params {
return NewParams(AssetParams{})
}
// NewAssetParam returns a new AssetParam
func NewAssetParam(
denom string, coinID int64, limit SupplyLimit, active bool,
deputyAddr sdk.AccAddress, fixedFee sdkmath.Int, minSwapAmount sdkmath.Int,
maxSwapAmount sdkmath.Int, minBlockLock uint64, maxBlockLock uint64,
) AssetParam {
return AssetParam{
Denom: denom,
CoinID: coinID,
SupplyLimit: limit,
Active: active,
DeputyAddress: deputyAddr,
FixedFee: fixedFee,
MinSwapAmount: minSwapAmount,
MaxSwapAmount: maxSwapAmount,
MinBlockLock: minBlockLock,
MaxBlockLock: maxBlockLock,
}
}
// AssetParams array of AssetParam
type AssetParams []AssetParam
// Equals returns true if two supply limits are equal
func (sl SupplyLimit) Equals(sl2 SupplyLimit) bool {
return sl.Limit.Equal(sl2.Limit) && sl.TimeLimited == sl2.TimeLimited && sl.TimePeriod == sl2.TimePeriod && sl.TimeBasedLimit.Equal(sl2.TimeBasedLimit)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() paramtypes.KeyTable {
return paramtypes.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
// pairs of bep3 module's parameters.
// nolint
func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs {
return paramtypes.ParamSetPairs{
paramtypes.NewParamSetPair(KeyAssetParams, &p.AssetParams, validateAssetParams),
}
}
// Validate ensure that params have valid values
func (p Params) Validate() error {
return validateAssetParams(p.AssetParams)
}
func validateAssetParams(i interface{}) error {
assetParams, ok := i.(AssetParams)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
coinDenoms := make(map[string]bool)
for _, asset := range assetParams {
if err := sdk.ValidateDenom(asset.Denom); err != nil {
return fmt.Errorf(fmt.Sprintf("asset denom invalid: %s", asset.Denom))
}
if asset.CoinID < 0 {
return fmt.Errorf(fmt.Sprintf("asset %s coin id must be a non negative integer", asset.Denom))
}
if asset.SupplyLimit.Limit.IsNegative() {
return fmt.Errorf(fmt.Sprintf("asset %s has invalid (negative) supply limit: %s", asset.Denom, asset.SupplyLimit.Limit))
}
if asset.SupplyLimit.TimeBasedLimit.IsNegative() {
return fmt.Errorf(fmt.Sprintf("asset %s has invalid (negative) supply time limit: %s", asset.Denom, asset.SupplyLimit.TimeBasedLimit))
}
if asset.SupplyLimit.TimeBasedLimit.GT(asset.SupplyLimit.Limit) {
return fmt.Errorf(fmt.Sprintf("asset %s cannot have supply time limit > supply limit: %s>%s", asset.Denom, asset.SupplyLimit.TimeBasedLimit, asset.SupplyLimit.Limit))
}
_, found := coinDenoms[asset.Denom]
if found {
return fmt.Errorf(fmt.Sprintf("asset %s cannot have duplicate denom", asset.Denom))
}
coinDenoms[asset.Denom] = true
if asset.DeputyAddress.Empty() {
return fmt.Errorf("deputy address cannot be empty for %s", asset.Denom)
}
if asset.FixedFee.IsNegative() {
return fmt.Errorf("asset %s cannot have a negative fixed fee %s", asset.Denom, asset.FixedFee)
}
if asset.MinBlockLock > asset.MaxBlockLock {
return fmt.Errorf("asset %s has minimum block lock > maximum block lock %d > %d", asset.Denom, asset.MinBlockLock, asset.MaxBlockLock)
}
if !asset.MinSwapAmount.IsPositive() {
return fmt.Errorf(fmt.Sprintf("asset %s must have a positive minimum swap amount, got %s", asset.Denom, asset.MinSwapAmount))
}
if !asset.MaxSwapAmount.IsPositive() {
return fmt.Errorf(fmt.Sprintf("asset %s must have a positive maximum swap amount, got %s", asset.Denom, asset.MaxSwapAmount))
}
if asset.MinSwapAmount.GT(asset.MaxSwapAmount) {
return fmt.Errorf("asset %s has minimum swap amount > maximum swap amount %s > %s", asset.Denom, asset.MinSwapAmount, asset.MaxSwapAmount)
}
}
return nil
}

249
x/bep3/types/params_test.go Normal file
View File

@ -0,0 +1,249 @@
package types_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type ParamsTestSuite struct {
suite.Suite
addr sdk.AccAddress
supply []types.SupplyLimit
}
func (suite *ParamsTestSuite) SetupTest() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
_, addrs := app.GeneratePrivKeyAddressPairs(1)
suite.addr = addrs[0]
supply1 := types.SupplyLimit{
Limit: sdkmath.NewInt(10000000000000),
TimeLimited: false,
TimeBasedLimit: sdk.ZeroInt(),
TimePeriod: time.Hour,
}
supply2 := types.SupplyLimit{
Limit: sdkmath.NewInt(10000000000000),
TimeLimited: true,
TimeBasedLimit: sdkmath.NewInt(100000000000),
TimePeriod: time.Hour * 24,
}
suite.supply = append(suite.supply, supply1, supply2)
}
func (suite *ParamsTestSuite) TestParamValidation() {
type args struct {
assetParams types.AssetParams
}
testCases := []struct {
name string
args args
expectPass bool
expectedErr string
}{
{
name: "default",
args: args{
assetParams: types.AssetParams{},
},
expectPass: true,
expectedErr: "",
},
{
name: "valid single asset",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: true,
expectedErr: "",
},
{
name: "valid single asset time limited",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[1], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: true,
expectedErr: "",
},
{
name: "valid multi asset",
args: args{
assetParams: types.AssetParams{
types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock),
types.NewAssetParam(
"btcb", 0, suite.supply[1], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(10000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock),
},
},
expectPass: true,
expectedErr: "",
},
{
name: "invalid denom - empty",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "denom invalid",
},
{
name: "min block lock equal max block lock",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
243, 243)},
},
expectPass: true,
expectedErr: "",
},
{
name: "min block lock greater max block lock",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
244, 243)},
},
expectPass: false,
expectedErr: "minimum block lock > maximum block lock",
},
{
name: "min swap not positive",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(0), sdkmath.NewInt(10000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "must have a positive minimum swap",
},
{
name: "max swap not positive",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(10000), sdkmath.NewInt(0),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "must have a positive maximum swap",
},
{
name: "min swap greater max swap",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000000), sdkmath.NewInt(10000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "minimum swap amount > maximum swap amount",
},
{
name: "negative coin id",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", -714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "coin id must be a non negative",
},
{
name: "negative asset limit",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714,
types.SupplyLimit{sdkmath.NewInt(-10000000000000), false, time.Hour, sdk.ZeroInt()}, true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "invalid (negative) supply limit",
},
{
name: "negative asset time limit",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714,
types.SupplyLimit{sdkmath.NewInt(10000000000000), false, time.Hour, sdkmath.NewInt(-10000000000000)}, true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "invalid (negative) supply time limit",
},
{
name: "asset time limit greater than overall limit",
args: args{
assetParams: types.AssetParams{types.NewAssetParam(
"bnb", 714,
types.SupplyLimit{sdkmath.NewInt(10000000000000), true, time.Hour, sdkmath.NewInt(100000000000000)},
true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock)},
},
expectPass: false,
expectedErr: "supply time limit > supply limit",
},
{
name: "duplicate denom",
args: args{
assetParams: types.AssetParams{
types.NewAssetParam(
"bnb", 714, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(100000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock),
types.NewAssetParam(
"bnb", 0, suite.supply[0], true,
suite.addr, sdkmath.NewInt(1000), sdkmath.NewInt(10000000), sdkmath.NewInt(100000000000),
types.DefaultMinBlockLock, types.DefaultMaxBlockLock),
},
},
expectPass: false,
expectedErr: "duplicate denom",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
params := types.NewParams(tc.args.assetParams)
err := params.Validate()
if tc.expectPass {
suite.Require().NoError(err, tc.name)
} else {
suite.Require().Error(err, tc.name)
suite.Require().Contains(err.Error(), tc.expectedErr)
}
})
}
}
func TestParamsTestSuite(t *testing.T) {
suite.Run(t, new(ParamsTestSuite))
}

3384
x/bep3/types/query.pb.go Normal file

File diff suppressed because it is too large Load Diff

503
x/bep3/types/query.pb.gw.go Normal file
View File

@ -0,0 +1,503 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: zgc/bep3/v1beta1/query.proto
/*
Package types is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package types
import (
"context"
"io"
"net/http"
"github.com/golang/protobuf/descriptor"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = descriptor.ForMessage
var _ = metadata.Join
func request_Query_Params_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryParamsRequest
var metadata runtime.ServerMetadata
msg, err := client.Params(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_Params_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryParamsRequest
var metadata runtime.ServerMetadata
msg, err := server.Params(ctx, &protoReq)
return msg, metadata, err
}
func request_Query_AssetSupply_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAssetSupplyRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["denom"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "denom")
}
protoReq.Denom, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "denom", err)
}
msg, err := client.AssetSupply(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_AssetSupply_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAssetSupplyRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["denom"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "denom")
}
protoReq.Denom, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "denom", err)
}
msg, err := server.AssetSupply(ctx, &protoReq)
return msg, metadata, err
}
func request_Query_AssetSupplies_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAssetSuppliesRequest
var metadata runtime.ServerMetadata
msg, err := client.AssetSupplies(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_AssetSupplies_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAssetSuppliesRequest
var metadata runtime.ServerMetadata
msg, err := server.AssetSupplies(ctx, &protoReq)
return msg, metadata, err
}
func request_Query_AtomicSwap_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAtomicSwapRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["swap_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "swap_id")
}
protoReq.SwapId, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "swap_id", err)
}
msg, err := client.AtomicSwap(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_AtomicSwap_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAtomicSwapRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["swap_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "swap_id")
}
protoReq.SwapId, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "swap_id", err)
}
msg, err := server.AtomicSwap(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_Query_AtomicSwaps_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_Query_AtomicSwaps_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAtomicSwapsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_AtomicSwaps_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.AtomicSwaps(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_AtomicSwaps_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryAtomicSwapsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_AtomicSwaps_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.AtomicSwaps(ctx, &protoReq)
return msg, metadata, err
}
// RegisterQueryHandlerServer registers the http handlers for service Query to "mux".
// UnaryRPC :call QueryServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterQueryHandlerFromEndpoint instead.
func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server QueryServer) error {
mux.Handle("GET", pattern_Query_Params_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_Params_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_Params_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AssetSupply_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_AssetSupply_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AssetSupply_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AssetSupplies_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_AssetSupplies_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AssetSupplies_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AtomicSwap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_AtomicSwap_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AtomicSwap_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AtomicSwaps_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_AtomicSwaps_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AtomicSwaps_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterQueryHandlerFromEndpoint is same as RegisterQueryHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterQueryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterQueryHandler(ctx, mux, conn)
}
// RegisterQueryHandler registers the http handlers for service Query to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterQueryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterQueryHandlerClient(ctx, mux, NewQueryClient(conn))
}
// RegisterQueryHandlerClient registers the http handlers for service Query
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "QueryClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "QueryClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "QueryClient" to call the correct interceptors.
func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client QueryClient) error {
mux.Handle("GET", pattern_Query_Params_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_Params_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_Params_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AssetSupply_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_AssetSupply_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AssetSupply_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AssetSupplies_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_AssetSupplies_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AssetSupplies_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AtomicSwap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_AtomicSwap_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AtomicSwap_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_AtomicSwaps_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_AtomicSwaps_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_AtomicSwaps_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Query_Params_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"0g-chain", "bep3", "v1beta1", "params"}, "", runtime.AssumeColonVerbOpt(false)))
pattern_Query_AssetSupply_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"0g-chain", "bep3", "v1beta1", "assetsupply", "denom"}, "", runtime.AssumeColonVerbOpt(false)))
pattern_Query_AssetSupplies_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"0g-chain", "bep3", "v1beta1", "assetsupplies"}, "", runtime.AssumeColonVerbOpt(false)))
pattern_Query_AtomicSwap_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"0g-chain", "bep3", "v1beta1", "atomicswap", "swap_id"}, "", runtime.AssumeColonVerbOpt(false)))
pattern_Query_AtomicSwaps_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"0g-chain", "bep3", "v1beta1", "atomicswaps"}, "", runtime.AssumeColonVerbOpt(false)))
)
var (
forward_Query_Params_0 = runtime.ForwardResponseMessage
forward_Query_AssetSupply_0 = runtime.ForwardResponseMessage
forward_Query_AssetSupplies_0 = runtime.ForwardResponseMessage
forward_Query_AtomicSwap_0 = runtime.ForwardResponseMessage
forward_Query_AtomicSwaps_0 = runtime.ForwardResponseMessage
)

64
x/bep3/types/supply.go Normal file
View File

@ -0,0 +1,64 @@
package types
import (
"fmt"
"time"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// NewAssetSupply initializes a new AssetSupply
func NewAssetSupply(incomingSupply, outgoingSupply, currentSupply, timeLimitedSupply sdk.Coin, timeElapsed time.Duration) AssetSupply {
return AssetSupply{
IncomingSupply: incomingSupply,
OutgoingSupply: outgoingSupply,
CurrentSupply: currentSupply,
TimeLimitedCurrentSupply: timeLimitedSupply,
TimeElapsed: timeElapsed,
}
}
// Validate performs a basic validation of an asset supply fields.
func (a AssetSupply) Validate() error {
if !a.IncomingSupply.IsValid() {
return errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "incoming supply %s", a.IncomingSupply)
}
if !a.OutgoingSupply.IsValid() {
return errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "outgoing supply %s", a.OutgoingSupply)
}
if !a.CurrentSupply.IsValid() {
return errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "current supply %s", a.CurrentSupply)
}
if !a.TimeLimitedCurrentSupply.IsValid() {
return errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "time-limited current supply %s", a.TimeLimitedCurrentSupply)
}
denom := a.CurrentSupply.Denom
if (a.IncomingSupply.Denom != denom) ||
(a.OutgoingSupply.Denom != denom) ||
(a.TimeLimitedCurrentSupply.Denom != denom) {
return fmt.Errorf("asset supply denoms do not match %s %s %s %s", a.CurrentSupply.Denom, a.IncomingSupply.Denom, a.OutgoingSupply.Denom, a.TimeLimitedCurrentSupply.Denom)
}
return nil
}
// Equal returns if two asset supplies are equal
func (a AssetSupply) Equal(b AssetSupply) bool {
if a.GetDenom() != b.GetDenom() {
return false
}
return (a.IncomingSupply.IsEqual(b.IncomingSupply) &&
a.CurrentSupply.IsEqual(b.CurrentSupply) &&
a.OutgoingSupply.IsEqual(b.OutgoingSupply) &&
a.TimeLimitedCurrentSupply.IsEqual(b.TimeLimitedCurrentSupply) &&
a.TimeElapsed == b.TimeElapsed)
}
// GetDenom getter method for the denom of the asset supply
func (a AssetSupply) GetDenom() string {
return a.CurrentSupply.Denom
}
// AssetSupplies is a slice of AssetSupply
type AssetSupplies []AssetSupply

122
x/bep3/types/supply_test.go Normal file
View File

@ -0,0 +1,122 @@
package types
import (
"testing"
"time"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestAssetSupplyValidate(t *testing.T) {
coin := sdk.NewCoin("a0gi", sdk.OneInt())
invalidCoin := sdk.Coin{Denom: "Invalid Denom", Amount: sdkmath.NewInt(-1)}
testCases := []struct {
msg string
asset AssetSupply
expPass bool
}{
{
msg: "valid asset",
asset: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
expPass: true,
},
{
"invalid incoming supply",
AssetSupply{IncomingSupply: invalidCoin},
false,
},
{
"invalid outgoing supply",
AssetSupply{
IncomingSupply: coin,
OutgoingSupply: invalidCoin,
},
false,
},
{
"invalid current supply",
AssetSupply{
IncomingSupply: coin,
OutgoingSupply: coin,
CurrentSupply: invalidCoin,
},
false,
},
{
"invalid time limitedcurrent supply",
AssetSupply{
IncomingSupply: coin,
OutgoingSupply: coin,
CurrentSupply: coin,
TimeLimitedCurrentSupply: invalidCoin,
},
false,
},
{
"non matching denoms",
AssetSupply{
IncomingSupply: coin,
OutgoingSupply: coin,
CurrentSupply: coin,
TimeLimitedCurrentSupply: sdk.NewCoin("lol", sdk.ZeroInt()),
TimeElapsed: time.Hour,
},
false,
},
}
for _, tc := range testCases {
err := tc.asset.Validate()
if tc.expPass {
require.NoError(t, err, tc.msg)
} else {
require.Error(t, err, tc.msg)
}
}
}
func TestAssetSupplyEquality(t *testing.T) {
coin := sdk.NewCoin("test", sdk.OneInt())
coin2 := sdk.NewCoin("other", sdk.OneInt())
testCases := []struct {
name string
asset1 AssetSupply
asset2 AssetSupply
expPass bool
}{
{
name: "equal",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
expPass: true,
},
{
name: "not equal duration",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(coin, coin, coin, coin, time.Duration(1)),
expPass: false,
},
{
name: "not equal coin amount",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(sdk.NewCoin("test", sdk.ZeroInt()), coin, coin, coin, time.Duration(1)),
expPass: false,
},
{
name: "not equal coin denom",
asset1: NewAssetSupply(coin, coin, coin, coin, time.Duration(0)),
asset2: NewAssetSupply(coin2, coin2, coin2, coin2, time.Duration(1)),
expPass: false,
},
}
for _, tc := range testCases {
if tc.expPass {
require.True(t, tc.asset1.Equal(tc.asset2), tc.name)
} else {
require.False(t, tc.asset1.Equal(tc.asset2), tc.name)
}
}
}

170
x/bep3/types/swap.go Normal file
View File

@ -0,0 +1,170 @@
package types
import (
"encoding/hex"
"errors"
"fmt"
"strings"
tmbytes "github.com/cometbft/cometbft/libs/bytes"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// NewAtomicSwap returns a new AtomicSwap
func NewAtomicSwap(amount sdk.Coins, randomNumberHash tmbytes.HexBytes, expireHeight uint64, timestamp int64,
sender, recipient sdk.AccAddress, senderOtherChain, recipientOtherChain string, closedBlock int64,
status SwapStatus, crossChain bool, direction SwapDirection,
) AtomicSwap {
return AtomicSwap{
Amount: amount,
RandomNumberHash: randomNumberHash,
ExpireHeight: expireHeight,
Timestamp: timestamp,
Sender: sender,
Recipient: recipient,
SenderOtherChain: senderOtherChain,
RecipientOtherChain: recipientOtherChain,
ClosedBlock: closedBlock,
Status: status,
CrossChain: crossChain,
Direction: direction,
}
}
// GetSwapID calculates the ID of an atomic swap
func (a AtomicSwap) GetSwapID() tmbytes.HexBytes {
return CalculateSwapID(a.RandomNumberHash, a.Sender, a.SenderOtherChain)
}
// GetCoins returns the swap's amount as sdk.Coins
func (a AtomicSwap) GetCoins() sdk.Coins {
return sdk.NewCoins(a.Amount...)
}
// Validate performs a basic validation of an atomic swap fields.
func (a AtomicSwap) Validate() error {
if !a.Amount.IsValid() {
return fmt.Errorf("invalid amount: %s", a.Amount)
}
if !a.Amount.IsAllPositive() {
return fmt.Errorf("the swapped out coin must be positive: %s", a.Amount)
}
if len(a.RandomNumberHash) != RandomNumberHashLength {
return fmt.Errorf("the length of random number hash should be %d", RandomNumberHashLength)
}
if a.ExpireHeight == 0 {
return errors.New("expire height cannot be 0")
}
if a.Timestamp == 0 {
return errors.New("timestamp cannot be 0")
}
if a.Sender.Empty() {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "sender cannot be empty")
}
if a.Recipient.Empty() {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "recipient cannot be empty")
}
// NOTE: These adresses may not have a bech32 prefix.
if strings.TrimSpace(a.SenderOtherChain) == "" {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "sender other chain cannot be blank")
}
if strings.TrimSpace(a.RecipientOtherChain) == "" {
return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "recipient other chain cannot be blank")
}
if a.Status == SWAP_STATUS_COMPLETED && a.ClosedBlock == 0 {
return errors.New("closed block cannot be 0")
}
if a.Status == SWAP_STATUS_UNSPECIFIED || a.Status > 3 {
return errors.New("invalid swap status")
}
if a.Direction == SWAP_DIRECTION_UNSPECIFIED || a.Direction > 2 {
return errors.New("invalid swap direction")
}
return nil
}
// AtomicSwaps is a slice of AtomicSwap
type AtomicSwaps []AtomicSwap
// NewSwapStatusFromString converts string to SwapStatus type
func NewSwapStatusFromString(str string) SwapStatus {
switch str {
case "Open", "open":
return SWAP_STATUS_OPEN
case "Completed", "completed":
return SWAP_STATUS_COMPLETED
case "Expired", "expired":
return SWAP_STATUS_EXPIRED
default:
return SWAP_STATUS_UNSPECIFIED
}
}
// IsValid returns true if the swap status is valid and false otherwise.
func (status SwapStatus) IsValid() bool {
return status == SWAP_STATUS_OPEN ||
status == SWAP_STATUS_COMPLETED ||
status == SWAP_STATUS_EXPIRED
}
// NewSwapDirectionFromString converts string to SwapDirection type
func NewSwapDirectionFromString(str string) SwapDirection {
switch str {
case "Incoming", "incoming", "inc", "I", "i":
return SWAP_DIRECTION_INCOMING
case "Outgoing", "outgoing", "out", "O", "o":
return SWAP_DIRECTION_OUTGOING
default:
return SWAP_DIRECTION_UNSPECIFIED
}
}
// IsValid returns true if the swap direction is valid and false otherwise.
func (direction SwapDirection) IsValid() bool {
return direction == SWAP_DIRECTION_INCOMING ||
direction == SWAP_DIRECTION_OUTGOING
}
// LegacyAugmentedAtomicSwap defines an ID and AtomicSwap fields on the top level.
// This should be removed when legacy REST endpoints are removed.
type LegacyAugmentedAtomicSwap struct {
ID string `json:"id" yaml:"id"`
// Embed AtomicSwap fields explicity in order to output as top level JSON fields
// This prevents breaking changes for clients using REST API
Amount sdk.Coins `json:"amount" yaml:"amount"`
RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"`
ExpireHeight uint64 `json:"expire_height" yaml:"expire_height"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"`
SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"`
RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"`
ClosedBlock int64 `json:"closed_block" yaml:"closed_block"`
Status SwapStatus `json:"status" yaml:"status"`
CrossChain bool `json:"cross_chain" yaml:"cross_chain"`
Direction SwapDirection `json:"direction" yaml:"direction"`
}
func NewLegacyAugmentedAtomicSwap(swap AtomicSwap) LegacyAugmentedAtomicSwap {
return LegacyAugmentedAtomicSwap{
ID: hex.EncodeToString(swap.GetSwapID()),
Amount: swap.Amount,
RandomNumberHash: swap.RandomNumberHash,
ExpireHeight: swap.ExpireHeight,
Timestamp: swap.Timestamp,
Sender: swap.Sender,
Recipient: swap.Recipient,
SenderOtherChain: swap.SenderOtherChain,
RecipientOtherChain: swap.RecipientOtherChain,
ClosedBlock: swap.ClosedBlock,
Status: swap.Status,
CrossChain: swap.CrossChain,
Direction: swap.Direction,
}
}
type LegacyAugmentedAtomicSwaps []LegacyAugmentedAtomicSwap

254
x/bep3/types/swap_test.go Normal file
View File

@ -0,0 +1,254 @@
package types_test
import (
"testing"
"github.com/stretchr/testify/suite"
tmbytes "github.com/cometbft/cometbft/libs/bytes"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/bep3/types"
)
type AtomicSwapTestSuite struct {
suite.Suite
addrs []sdk.AccAddress
timestamps []int64
randomNumberHashes []tmbytes.HexBytes
}
func (suite *AtomicSwapTestSuite) SetupTest() {
// Generate 10 addresses
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
_, addrs := app.GeneratePrivKeyAddressPairs(10)
// Generate 10 timestamps and random number hashes
var timestamps []int64
var randomNumberHashes []tmbytes.HexBytes
for i := 0; i < 10; i++ {
timestamp := ts(i)
randomNumber, _ := types.GenerateSecureRandomNumber()
randomNumberHash := types.CalculateRandomHash(randomNumber[:], timestamp)
timestamps = append(timestamps, timestamp)
randomNumberHashes = append(randomNumberHashes, randomNumberHash)
}
suite.addrs = addrs
suite.timestamps = timestamps
suite.randomNumberHashes = randomNumberHashes
}
func (suite *AtomicSwapTestSuite) TestNewAtomicSwap() {
testCases := []struct {
msg string
swap types.AtomicSwap
expectPass bool
}{
{
"valid Swap",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 360,
Timestamp: suite.timestamps[0],
Sender: suite.addrs[0],
Recipient: suite.addrs[5],
RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7",
SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
ClosedBlock: 1,
Status: types.SWAP_STATUS_OPEN,
CrossChain: true,
Direction: types.SWAP_DIRECTION_INCOMING,
},
true,
},
{
"invalid amount",
types.AtomicSwap{
Amount: sdk.Coins{sdk.Coin{Denom: "BNB", Amount: sdkmath.NewInt(10)}},
},
false,
},
{
"amount not positive",
types.AtomicSwap{
Amount: cs(c("bnb", 0)),
},
false,
},
{
"invalid random number hash length",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[1][0:20],
},
false,
},
{
"exp height 0",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 0,
},
false,
},
{
"timestamp 0",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 0,
},
false,
},
{
"empty sender",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: nil,
},
false,
},
{
"empty recipient",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0],
Recipient: nil,
},
false,
},
{
"invalid sender length",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0][:10],
Recipient: suite.addrs[5],
},
false,
},
{
"invalid recipient length",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0],
Recipient: suite.addrs[5][:10],
},
false,
},
{
"invalid sender other chain",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0],
Recipient: suite.addrs[5],
SenderOtherChain: "",
},
false,
},
{
"invalid recipient other chain",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0],
Recipient: suite.addrs[5],
SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
RecipientOtherChain: "",
},
false,
},
{
"closed block 0",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0],
Recipient: suite.addrs[5],
SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7",
ClosedBlock: 0,
Status: types.SWAP_STATUS_COMPLETED,
},
false,
},
{
"invalid status 0",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0],
Recipient: suite.addrs[5],
SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7",
ClosedBlock: 1,
Status: types.SWAP_STATUS_UNSPECIFIED,
},
false,
},
{
"invalid direction ",
types.AtomicSwap{
Amount: cs(c("bnb", 50000)),
RandomNumberHash: suite.randomNumberHashes[0],
ExpireHeight: 10,
Timestamp: 10,
Sender: suite.addrs[0],
Recipient: suite.addrs[5],
SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7",
RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7",
ClosedBlock: 1,
Status: types.SWAP_STATUS_OPEN,
Direction: types.SWAP_DIRECTION_UNSPECIFIED,
},
false,
},
}
for _, tc := range testCases {
err := tc.swap.Validate()
if tc.expectPass {
suite.Require().NoError(err, tc.msg)
suite.Require().Equal(tc.swap.Amount, tc.swap.GetCoins())
expectedSwapID := types.CalculateSwapID(tc.swap.RandomNumberHash, tc.swap.Sender, tc.swap.SenderOtherChain)
suite.Require().Equal(tmbytes.HexBytes(expectedSwapID), tc.swap.GetSwapID())
} else {
suite.Require().Error(err)
}
}
}
func TestAtomicSwapTestSuite(t *testing.T) {
suite.Run(t, new(AtomicSwapTestSuite))
}

1607
x/bep3/types/tx.pb.go Normal file

File diff suppressed because it is too large Load Diff

20
x/committee/abci.go Normal file
View File

@ -0,0 +1,20 @@
package committee
import (
"time"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/types"
)
// BeginBlocker runs at the start of every block.
func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k keeper.Keeper) {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker)
k.ProcessProposals(ctx)
}

228
x/committee/abci_test.go Normal file
View File

@ -0,0 +1,228 @@
package committee_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
abci "github.com/cometbft/cometbft/abci/types"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"github.com/0glabs/0g-chain/app"
// "github.com/0glabs/0g-chain/x/cdp"
// cdptypes "github.com/0glabs/0g-chain/x/cdp/types"
"github.com/0glabs/0g-chain/x/committee"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/testutil"
"github.com/0glabs/0g-chain/x/committee/types"
)
type ModuleTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addresses []sdk.AccAddress
}
func (suite *ModuleTestSuite) SetupTest() {
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, tmproto.Header{})
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
}
func (suite *ModuleTestSuite) TestBeginBlock_ClosesExpired() {
suite.app.InitializeFromGenesisStates()
memberCom := types.MustNewMemberCommittee(
12,
"This committee is for testing.",
suite.addresses[:2],
[]types.Permission{&types.GodPermission{}},
testutil.D("0.8"),
time.Hour*24*7,
types.TALLY_OPTION_DEADLINE,
)
suite.keeper.SetCommittee(suite.ctx, memberCom)
pprop1 := govv1beta1.NewTextProposal("Title 1", "A description of this proposal.")
id1, err := suite.keeper.SubmitProposal(suite.ctx, memberCom.Members[0], memberCom.ID, pprop1)
suite.NoError(err)
oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour))
pprop2 := govv1beta1.NewTextProposal("Title 2", "A description of this proposal.")
id2, err := suite.keeper.SubmitProposal(oneHrLaterCtx, memberCom.Members[0], memberCom.ID, pprop2)
suite.NoError(err)
// Run BeginBlocker
proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(memberCom.ProposalDuration))
suite.NotPanics(func() {
committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper)
})
// Check expired proposals are gone
_, found := suite.keeper.GetProposal(suite.ctx, id1)
suite.False(found, "expected expired proposal to be closed")
_, found = suite.keeper.GetProposal(suite.ctx, id2)
suite.True(found, "expected non expired proposal to be not closed")
}
// func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassed() {
// suite.app.InitializeFromGenesisStates()
// // setup committee
// normalCom := types.MustNewMemberCommittee(12, "committee description", suite.addresses[:2],
// []types.Permission{&types.GodPermission{}}, testutil.D("0.8"), time.Hour*24*7, types.TALLY_OPTION_FIRST_PAST_THE_POST)
// suite.keeper.SetCommittee(suite.ctx, normalCom)
// // setup 2 proposals
// previousCDPDebtThreshold := suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold
// newDebtThreshold := previousCDPDebtThreshold.Add(i(1000000))
// evenNewerDebtThreshold := newDebtThreshold.Add(i(1000000))
// pprop1 := params.NewParameterChangeProposal("Title 1", "A description of this proposal.",
// []params.ParamChange{{
// Subspace: cdptypes.ModuleName,
// Key: string(cdp.KeyDebtThreshold),
// Value: string(cdp.ModuleCdc.MustMarshalJSON(newDebtThreshold)),
// }},
// )
// id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1)
// suite.NoError(err)
// pprop2 := params.NewParameterChangeProposal("Title 2", "A description of this proposal.",
// []params.ParamChange{{
// Subspace: cdptypes.ModuleName,
// Key: string(cdp.KeyDebtThreshold),
// Value: string(cdp.ModuleCdc.MustMarshalJSON(evenNewerDebtThreshold)),
// }},
// )
// id2, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop2)
// suite.NoError(err)
// // add enough votes to make the first proposal pass, but not the second
// suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[0], types.Yes))
// suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[1], types.Yes))
// suite.NoError(suite.keeper.AddVote(suite.ctx, id2, suite.addresses[0], types.Yes))
// // Run BeginBlocker
// suite.NotPanics(func() {
// committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.keeper)
// })
// // Check the param has been updated
// suite.Equal(newDebtThreshold, suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold)
// // Check the passed proposal has gone
// _, found := suite.keeper.GetProposal(suite.ctx, id1)
// suite.False(found, "expected passed proposal to be enacted and closed")
// _, found = suite.keeper.GetProposal(suite.ctx, id2)
// suite.True(found, "expected non passed proposal to be not closed")
// }
// func (suite *ModuleTestSuite) TestBeginBlock_DoesntEnactFailed() {
// suite.app.InitializeFromGenesisStates()
// // setup committee
// memberCom := types.MustNewMemberCommittee(12, "committee description", suite.addresses[:1],
// []types.Permission{types.SoftwareUpgradePermission{}}, testutil.D("1.0"), time.Hour*24*7, types.TALLY_OPTION_FIRST_PAST_THE_POST)
// firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
// ctx := suite.ctx.WithBlockTime(firstBlockTime)
// suite.keeper.SetCommittee(ctx, memberCom)
// // setup an upgrade proposal
// pprop1 := upgradetypes.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.",
// upgradetypes.Plan{
// Name: "upgrade-version-v0.23.1",
// Time: firstBlockTime.Add(time.Second * 5),
// Info: "some information about the upgrade",
// },
// )
// id1, err := suite.keeper.SubmitProposal(ctx, memberCom.Members[0], memberCom.ID, pprop1)
// suite.NoError(err)
// // add enough votes to make the proposal pass
// suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0], types.Yes))
// // Run BeginBlocker 10 seconds later (5 seconds after upgrade expires)
// tenSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 10))
// suite.NotPanics(func() {
// suite.app.BeginBlocker(tenSecLaterCtx, abci.RequestBeginBlock{})
// })
// // Check the plan has not been stored
// _, found := suite.app.GetUpgradeKeeper().GetUpgradePlan(tenSecLaterCtx)
// suite.False(found)
// // Check the passed proposal has gone
// _, found = suite.keeper.GetProposal(tenSecLaterCtx, id1)
// suite.False(found, "expected failed proposal to be not enacted and closed")
// // Check the chain doesn't halt
// oneMinLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Minute).Add(time.Second))
// suite.NotPanics(func() {
// suite.app.BeginBlocker(oneMinLaterCtx, abci.RequestBeginBlock{})
// })
// }
// func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassedUpgrade() {
// suite.app.InitializeFromGenesisStates()
// // setup committee
// memberCom := types.MustNewMemberCommittee(
// 12,
// "committee description",
// suite.addresses[:1],
// []types.Permission{types.SoftwareUpgradePermission{}},
// testutil.D("1.0"),
// time.Hour*24*7,
// types.TALLY_OPTION_FIRST_PAST_THE_POST,
// )
// firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
// ctx := suite.ctx.WithBlockTime(firstBlockTime)
// suite.keeper.SetCommittee(ctx, memberCom)
// // setup an upgrade proposal
// pprop1 := upgradetypes.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.",
// upgradetypes.Plan{
// Name: "upgrade-version-v0.23.1",
// Time: firstBlockTime.Add(time.Minute * 1),
// Info: "some information about the upgrade",
// },
// )
// id1, err := suite.keeper.SubmitProposal(ctx, memberCom.Members[0], memberCom.ID, pprop1)
// suite.NoError(err)
// // add enough votes to make the proposal pass
// suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0], types.Yes))
// // Run BeginBlocker
// fiveSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 5))
// suite.NotPanics(func() {
// suite.app.BeginBlocker(fiveSecLaterCtx, abci.RequestBeginBlock{})
// })
// // Check the plan has been stored
// _, found := suite.app.GetUpgradeKeeper().GetUpgradePlan(fiveSecLaterCtx)
// suite.True(found)
// // Check the passed proposal has gone
// _, found = suite.keeper.GetProposal(fiveSecLaterCtx, id1)
// suite.False(found, "expected passed proposal to be enacted and closed")
// // Check the chain halts
// oneMinLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Minute))
// suite.Panics(func() {
// suite.app.BeginBlocker(oneMinLaterCtx, abci.RequestBeginBlock{})
// })
// }
func TestModuleTestSuite(t *testing.T) {
suite.Run(t, new(ModuleTestSuite))
}

View File

@ -0,0 +1,38 @@
package cli_test
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/committee/client/cli"
)
type CLITestSuite struct {
suite.Suite
cdc codec.Codec
}
func (suite *CLITestSuite) SetupTest() {
tApp := app.NewTestApp()
suite.cdc = tApp.AppCodec()
}
func (suite *CLITestSuite) TestExampleCommitteeChangeProposal_NotPanics() {
suite.NotPanics(func() { cli.MustGetExampleCommitteeChangeProposal(suite.cdc) })
}
func (suite *CLITestSuite) TestExampleCommitteeDeleteProposal_NotPanics() {
suite.NotPanics(func() { cli.MustGetExampleCommitteeDeleteProposal(suite.cdc) })
}
func (suite *CLITestSuite) TestExampleParameterChangeProposal_NotPanics() {
suite.NotPanics(func() { cli.MustGetExampleParameterChangeProposal(suite.cdc) })
}
func TestCLITestSuite(t *testing.T) {
suite.Run(t, new(CLITestSuite))
}

View File

@ -0,0 +1,318 @@
package cli
import (
"context"
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/0glabs/0g-chain/x/committee/client/common"
"github.com/0glabs/0g-chain/x/committee/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/version"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd() *cobra.Command {
queryCmd := &cobra.Command{
Use: types.ModuleName,
Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
cmds := []*cobra.Command{
// committees
getCmdQueryCommittee(),
getCmdQueryCommittees(),
// proposals
getCmdQueryNextProposalID(),
getCmdQueryProposal(),
getCmdQueryProposals(),
// votes
getCmdQueryVotes(),
// other
getCmdQueryProposer(),
getCmdQueryTally(),
getCmdQueryRawParams(),
}
for _, cmd := range cmds {
flags.AddQueryFlagsToCmd(cmd)
}
queryCmd.AddCommand(cmds...)
return queryCmd
}
// ------------------------------------------
// Committees
// ------------------------------------------
// getCmdQueryCommittee implements a query committee command.
func getCmdQueryCommittee() *cobra.Command {
return &cobra.Command{
Use: "committee [committee-id]",
Args: cobra.ExactArgs(1),
Short: "Query details of a single committee",
Example: fmt.Sprintf("%s query %s committee 1", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
// validate that the committee id is a uint
committeeID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("committee-id %s not a valid uint, please input a valid committee-id", args[0])
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.Committee(context.Background(), &types.QueryCommitteeRequest{CommitteeId: committeeID})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// getCmdQueryCommittees implements a query committees command.
func getCmdQueryCommittees() *cobra.Command {
return &cobra.Command{
Use: "committees",
Args: cobra.NoArgs,
Short: "Query all committees",
Example: fmt.Sprintf("%s query %s committees", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.Committees(context.Background(), &types.QueryCommitteesRequest{})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// ------------------------------------------
// Proposals
// ------------------------------------------
// getCmdQueryNextProposalID implements a query next proposal ID command.
func getCmdQueryNextProposalID() *cobra.Command {
return &cobra.Command{
Use: "next-proposal-id",
Short: "Query the next proposal ID",
Args: cobra.ExactArgs(0),
Example: fmt.Sprintf("%s query %s next-proposal-id", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.NextProposalID(context.Background(), &types.QueryNextProposalIDRequest{})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// getCmdQueryProposal implements the query proposal command.
func getCmdQueryProposal() *cobra.Command {
return &cobra.Command{
Use: "proposal [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Query details of a single proposal",
Example: fmt.Sprintf("%s query %s proposal 2", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
// Prepare params for querier
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid uint", args[0])
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.Proposal(context.Background(), &types.QueryProposalRequest{
ProposalId: proposalID,
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// getCmdQueryProposals implements a query proposals command.
func getCmdQueryProposals() *cobra.Command {
return &cobra.Command{
Use: "proposals [committee-id]",
Short: "Query all proposals for a committee",
Args: cobra.ExactArgs(1),
Example: fmt.Sprintf("%s query %s proposals 1", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
// Prepare params for querier
committeeID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("committee-id %s not a valid uint", args[0])
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.Proposals(context.Background(), &types.QueryProposalsRequest{
CommitteeId: committeeID,
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// ------------------------------------------
// Votes
// ------------------------------------------
// getCmdQueryVotes implements the command to query for proposal votes.
func getCmdQueryVotes() *cobra.Command {
return &cobra.Command{
Use: "votes [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Query votes on a proposal",
Example: fmt.Sprintf("%s query %s votes 2", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
// Prepare params for querier
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int", args[0])
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.Votes(context.Background(), &types.QueryVotesRequest{
ProposalId: proposalID,
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
// ------------------------------------------
// Other
// ------------------------------------------
func getCmdQueryTally() *cobra.Command {
return &cobra.Command{
Use: "tally [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Get the current tally of votes on a proposal",
Long: "Query the current tally of votes on a proposal to see the progress of the voting.",
Example: fmt.Sprintf("%s query %s tally 2", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
// Prepare params for querier
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int", args[0])
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.Tally(context.Background(), &types.QueryTallyRequest{
ProposalId: proposalID,
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}
func getCmdQueryProposer() *cobra.Command {
return &cobra.Command{
Use: "proposer [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Query the proposer of a governance proposal",
Long: "Query which address proposed a proposal with a given ID.",
Example: fmt.Sprintf("%s query %s proposer 2", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
// validate that the proposalID is a uint
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s is not a valid uint", args[0])
}
prop, err := common.QueryProposer(clientCtx, proposalID)
if err != nil {
return err
}
return clientCtx.PrintObjectLegacy(prop)
},
}
}
func getCmdQueryRawParams() *cobra.Command {
return &cobra.Command{
Use: "raw-params [subspace] [key]",
Args: cobra.ExactArgs(2),
Short: "Query raw parameter values from any module.",
Long: "Query the byte value of any module's parameters. Useful in debugging and verifying governance proposals.",
Example: fmt.Sprintf("%s query %s raw-params cdp CollateralParams", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.RawParams(context.Background(), &types.QueryRawParamsRequest{
Subspace: args[0],
Key: args[1],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
}

View File

@ -0,0 +1,325 @@
package cli
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"strconv"
"strings"
"time"
"github.com/cometbft/cometbft/crypto"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
paramsproposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal"
"github.com/0glabs/0g-chain/x/committee/types"
)
const PARAMS_CHANGE_PROPOSAL_EXAMPLE = `
{
"@type": "/cosmos.params.v1beta1.ParameterChangeProposal",
"title": "title",
"description": "description",
"changes": [{ "subspace": "subspace", "key": "key", "value": "value" }]
}
`
const COMMITTEE_CHANGE_PROPOSAL_EXAMPLE = `
{
"@type": "/0g-chain.committee.v1beta1.CommitteeChangeProposal",
"title": "A Title",
"description": "A proposal description.",
"new_committee": {
"@type": "/0g-chain.committee.v1beta1.MemberCommittee",
"base_committee": {
"id": "34",
"description": "member committee",
"members": ["0g1ze7y9qwdddejmy7jlw4cymqqlt2wh05yhwmrv2"],
"permissions": [],
"vote_threshold": "1.000000000000000000",
"proposal_duration": "86400s",
"tally_option": "TALLY_OPTION_DEADLINE"
}
}
}
`
const COMMITTEE_DELETE_PROPOSAL_EXAMPLE = `
{
"@type": "/0g-chain.committee.v1beta1.CommitteeDeleteProposal",
"title": "A Title",
"description": "A proposal description.",
"committee_id": "1"
}
`
func GetTxCmd() *cobra.Command {
txCmd := &cobra.Command{
Use: types.ModuleName,
Short: "committee governance transactions subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
cmds := []*cobra.Command{
getCmdVote(),
getCmdSubmitProposal(),
}
for _, cmd := range cmds {
flags.AddTxFlagsToCmd(cmd)
}
txCmd.AddCommand(cmds...)
return txCmd
}
// getCmdSubmitProposal returns the command to submit a proposal to a committee
func getCmdSubmitProposal() *cobra.Command {
cmd := &cobra.Command{
Use: "submit-proposal [committee-id] [proposal-file]",
Short: "Submit a governance proposal to a particular committee",
Long: fmt.Sprintf(`Submit a proposal to a committee so they can vote on it.
The proposal file must be the json encoded forms of the proposal type you want to submit.
For example:
%s
`, PARAMS_CHANGE_PROPOSAL_EXAMPLE),
Args: cobra.ExactArgs(2),
Example: fmt.Sprintf("%s tx %s submit-proposal 1 your-proposal.json", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
// Get proposing address
proposer := clientCtx.GetFromAddress()
// Get committee ID
committeeID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("committee-id %s not a valid int", args[0])
}
// Get proposal content
var pubProposal types.PubProposal
contents, err := ioutil.ReadFile(args[1])
if err != nil {
return err
}
if err := clientCtx.Codec.UnmarshalInterfaceJSON(contents, &pubProposal); err != nil {
return err
}
if err = pubProposal.ValidateBasic(); err != nil {
return err
}
// Build message and run basic validation
msg, err := types.NewMsgSubmitProposal(pubProposal, proposer, committeeID)
if err != nil {
return err
}
err = msg.ValidateBasic()
if err != nil {
return err
}
// Sign and broadcast message
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
return cmd
}
// getCmdVote returns the command to vote on a proposal.
func getCmdVote() *cobra.Command {
return &cobra.Command{
Use: "vote [proposal-id] [vote]",
Args: cobra.ExactArgs(2),
Short: "Vote for an active proposal",
Long: "Submit a [yes/no/abstain] vote for the proposal with id [proposal-id].",
Example: fmt.Sprintf("%s tx %s vote 2 yes", version.AppName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
// Get voting address
from := clientCtx.GetFromAddress()
// validate that the proposal id is a uint
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0])
}
rawVote := strings.ToLower(strings.TrimSpace(args[1]))
if len(rawVote) == 0 {
return fmt.Errorf("must specify a vote")
}
var vote types.VoteType
switch rawVote {
case "yes", "y":
vote = types.VOTE_TYPE_YES
case "no", "n":
vote = types.VOTE_TYPE_NO
case "abstain", "a":
vote = types.VOTE_TYPE_ABSTAIN
default:
return fmt.Errorf("must specify a valid vote type: (yes/y, no/n, abstain/a)")
}
// Build vote message and run basic validation
msg := types.NewMsgVote(from, proposalID, vote)
err = msg.ValidateBasic()
if err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
}
// GetGovCmdSubmitProposal returns a command to submit a proposal to the gov module. It is passed to the gov module for use on its command subtree.
func GetGovCmdSubmitProposal() *cobra.Command {
cmd := &cobra.Command{
Use: "committee [proposal-file] [deposit]",
Short: "Submit a governance proposal to change a committee.",
Long: fmt.Sprintf(`Submit a governance proposal to create, alter, or delete a committee.
The proposal file must be the json encoded form of the proposal type you want to submit.
For example, to create or update a committee:
%s
and to delete a committee:
%s
`, COMMITTEE_CHANGE_PROPOSAL_EXAMPLE, COMMITTEE_DELETE_PROPOSAL_EXAMPLE),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
// Get proposing address
proposer := clientCtx.GetFromAddress()
// Get the deposit
deposit, err := sdk.ParseCoinsNormalized(args[1])
if err != nil {
return err
}
// Get the proposal
bz, err := ioutil.ReadFile(args[0])
if err != nil {
return err
}
var content govv1beta1.Content
if err := clientCtx.Codec.UnmarshalInterfaceJSON(bz, &content); err != nil {
return err
}
if err = content.ValidateBasic(); err != nil {
return err
}
// Build message and run basic validation
msg, err := govv1beta1.NewMsgSubmitProposal(content, deposit, proposer)
if err != nil {
return err
}
err = msg.ValidateBasic()
if err != nil {
return err
}
// Sign and broadcast message
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
return cmd
}
// MustGetExampleCommitteeChangeProposal is a helper function to return an example json proposal
func MustGetExampleCommitteeChangeProposal(cdc codec.Codec) string {
exampleChangeProposal, err := types.NewCommitteeChangeProposal(
"A Title",
"A description of this proposal.",
types.MustNewMemberCommittee(
1,
"The description of this committee.",
[]sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))},
[]types.Permission{
&types.GodPermission{},
},
sdk.MustNewDecFromStr("0.8"),
time.Hour*24*7,
types.TALLY_OPTION_FIRST_PAST_THE_POST,
),
)
if err != nil {
panic(err)
}
exampleChangeProposalBz, err := cdc.MarshalJSON(&exampleChangeProposal)
if err != nil {
panic(err)
}
var out bytes.Buffer
if err = json.Indent(&out, exampleChangeProposalBz, "", " "); err != nil {
panic(err)
}
return out.String()
}
// MustGetExampleCommitteeDeleteProposal is a helper function to return an example json proposal
func MustGetExampleCommitteeDeleteProposal(cdc codec.Codec) string {
exampleDeleteProposal := types.NewCommitteeDeleteProposal(
"A Title",
"A description of this proposal.",
1,
)
bz, err := cdc.MarshalJSON(&exampleDeleteProposal)
if err != nil {
panic(err)
}
var out bytes.Buffer
if err = json.Indent(&out, bz, "", " "); err != nil {
panic(err)
}
return out.String()
}
// MustGetExampleParameterChangeProposal is a helper function to return an example json proposal
func MustGetExampleParameterChangeProposal(cdc codec.Codec) string {
value := fmt.Sprintf("\"%d\"", 1000000000)
exampleParameterChangeProposal := paramsproposal.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]paramsproposal.ParamChange{paramsproposal.NewParamChange("cdp", "SurplusAuctionThreshold", value)},
)
bz, err := cdc.MarshalJSON(exampleParameterChangeProposal)
if err != nil {
panic(err)
}
var out bytes.Buffer
if err = json.Indent(&out, bz, "", " "); err != nil {
panic(err)
}
return out.String()
}

View File

@ -0,0 +1,59 @@
package common
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
"github.com/0glabs/0g-chain/x/committee/types"
)
// Note: QueryProposer is copied in from the gov module
const (
defaultPage = 1
defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19
)
// Proposer contains metadata of a governance proposal used for querying a proposer.
type Proposer struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Proposer string `json:"proposer" yaml:"proposer"`
}
// NewProposer returns a new Proposer given id and proposer
func NewProposer(proposalID uint64, proposer string) Proposer {
return Proposer{proposalID, proposer}
}
func (p Proposer) String() string {
return fmt.Sprintf("Proposal with ID %d was proposed by %s", p.ProposalID, p.Proposer)
}
// QueryProposer will query for a proposer of a governance proposal by ID.
func QueryProposer(cliCtx client.Context, proposalID uint64) (Proposer, error) {
events := []string{
fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal),
fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalSubmit, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))),
}
// NOTE: SearchTxs is used to facilitate the txs query which does not currently
// support configurable pagination.
searchResult, err := authtx.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit, "")
if err != nil {
return Proposer{}, err
}
for _, info := range searchResult.Txs {
for _, msg := range info.GetTx().GetMsgs() {
// there should only be a single proposal under the given conditions
if subMsg, ok := msg.(*types.MsgSubmitProposal); ok {
return NewProposer(proposalID, subMsg.Proposer), nil
}
}
}
return Proposer{}, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID)
}

View File

@ -0,0 +1,10 @@
package client
import (
govclient "github.com/cosmos/cosmos-sdk/x/gov/client"
"github.com/0glabs/0g-chain/x/committee/client/cli"
)
// ProposalHandler is a struct containing handler funcs for submiting CommitteeChange/Delete proposal txs to the gov module through the cli or rest.
var ProposalHandler = govclient.NewProposalHandler(cli.GetGovCmdSubmitProposal)

47
x/committee/genesis.go Normal file
View File

@ -0,0 +1,47 @@
package committee
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, gs *types.GenesisState) {
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err))
}
keeper.SetNextProposalID(ctx, gs.NextProposalID)
for _, com := range gs.GetCommittees() {
keeper.SetCommittee(ctx, com)
}
for _, p := range gs.Proposals {
keeper.SetProposal(ctx, p)
}
for _, v := range gs.Votes {
keeper.SetVote(ctx, v)
}
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState {
nextID, err := keeper.GetNextProposalID(ctx)
if err != nil {
panic(err)
}
committees := keeper.GetCommittees(ctx)
proposals := keeper.GetProposals(ctx)
votes := keeper.GetVotes(ctx)
return types.NewGenesisState(
nextID,
committees,
proposals,
votes,
)
}

163
x/committee/genesis_test.go Normal file
View File

@ -0,0 +1,163 @@
package committee_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/committee"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/testutil"
"github.com/0glabs/0g-chain/x/committee/types"
)
type GenesisTestSuite struct {
suite.Suite
app app.TestApp
ctx sdk.Context
keeper keeper.Keeper
addresses []sdk.AccAddress
}
func (suite *GenesisTestSuite) SetupTest() {
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, tmproto.Header{})
_, suite.addresses = app.GeneratePrivKeyAddressPairs(10)
}
func (suite *GenesisTestSuite) TestInitGenesis() {
memberCom := types.MustNewMemberCommittee(
1,
"This member committee is for testing.",
suite.addresses[:2],
[]types.Permission{&types.GodPermission{}},
testutil.D("0.667"),
time.Hour*24*7,
types.TALLY_OPTION_FIRST_PAST_THE_POST,
)
tokenCom := types.MustNewTokenCommittee(
1,
"This token committee is for testing.",
suite.addresses[:2],
[]types.Permission{&types.GodPermission{}},
testutil.D("0.667"),
time.Hour*24*7,
types.TALLY_OPTION_FIRST_PAST_THE_POST,
testutil.D("0.4"),
"hard",
)
// Most genesis validation tests are located in the types directory. The 'invalid' test cases are
// randomly selected subset of those tests.
testCases := []struct {
name string
genState *types.GenesisState
expectPass bool
}{
{
name: "normal",
genState: types.DefaultGenesisState(),
expectPass: true,
},
{
name: "member committee is correctly validated",
genState: types.NewGenesisState(
1,
[]types.Committee{memberCom},
[]types.Proposal{},
[]types.Vote{},
),
expectPass: true,
},
{
name: "token committee is correctly validated",
genState: types.NewGenesisState(
1,
[]types.Committee{tokenCom},
[]types.Proposal{},
[]types.Vote{},
),
expectPass: true,
},
{
name: "invalid: duplicate committee ID",
genState: types.NewGenesisState(
1,
[]types.Committee{memberCom, memberCom},
[]types.Proposal{},
[]types.Vote{},
),
expectPass: false,
},
{
name: "invalid: proposal doesn't have committee",
genState: types.NewGenesisState(
2,
[]types.Committee{},
[]types.Proposal{{ID: 1, CommitteeID: 57}},
[]types.Vote{},
),
expectPass: false,
},
{
name: "invalid: vote doesn't have proposal",
genState: types.NewGenesisState(
1,
[]types.Committee{},
[]types.Proposal{},
[]types.Vote{{Voter: suite.addresses[0], ProposalID: 1, VoteType: types.VOTE_TYPE_YES}},
),
expectPass: false,
},
{
name: "invalid: next proposal ID isn't greater than proposal ID",
genState: types.NewGenesisState(
4,
[]types.Committee{memberCom},
[]types.Proposal{{ID: 3, CommitteeID: 1}, {ID: 4, CommitteeID: 1}},
[]types.Vote{},
),
expectPass: false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Setup (note: suite.SetupTest is not run before every suite.Run)
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, tmproto.Header{})
// Run
var exportedGenState *types.GenesisState
run := func() {
committee.InitGenesis(suite.ctx, suite.keeper, tc.genState)
exportedGenState = committee.ExportGenesis(suite.ctx, suite.keeper)
}
if tc.expectPass {
suite.Require().NotPanics(run)
} else {
suite.Require().Panics(run)
}
// Check
if tc.expectPass {
expectedJson, err := suite.app.AppCodec().MarshalJSON(tc.genState)
suite.Require().NoError(err)
actualJson, err := suite.app.AppCodec().MarshalJSON(exportedGenState)
suite.Equal(expectedJson, actualJson)
}
})
}
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

View File

@ -0,0 +1,258 @@
package keeper_test
import (
"testing"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/stretchr/testify/suite"
"github.com/0glabs/0g-chain/app"
)
type PermissionTestSuite struct {
suite.Suite
cdc codec.Codec
}
func (suite *PermissionTestSuite) SetupTest() {
app := app.NewTestApp()
suite.cdc = app.AppCodec()
}
// func (suite *PermissionTestSuite) TestSubParamChangePermission_Allows() {
// // cdp CollateralParams
// testCPs := cdptypes.CollateralParams{
// {
// Denom: "bnb",
// Type: "bnb-a",
// LiquidationRatio: d("2.0"),
// DebtLimit: c("usdx", 1000000000000),
// StabilityFee: d("1.000000001547125958"),
// LiquidationPenalty: d("0.05"),
// AuctionSize: i(100),
// Prefix: 0x20,
// ConversionFactor: i(6),
// SpotMarketID: "bnb:usd",
// LiquidationMarketID: "bnb:usd",
// },
// {
// Denom: "btc",
// Type: "btc-a",
// LiquidationRatio: d("1.5"),
// DebtLimit: c("usdx", 1000000000),
// StabilityFee: d("1.000000001547125958"),
// LiquidationPenalty: d("0.10"),
// AuctionSize: i(1000),
// Prefix: 0x30,
// ConversionFactor: i(8),
// SpotMarketID: "btc:usd",
// LiquidationMarketID: "btc:usd",
// },
// }
// testCPUpdatedDebtLimit := make(cdptypes.CollateralParams, len(testCPs))
// copy(testCPUpdatedDebtLimit, testCPs)
// testCPUpdatedDebtLimit[0].DebtLimit = c("usdx", 5000000)
// // cdp DebtParam
// testDP := cdptypes.DebtParam{
// Denom: "usdx",
// ReferenceAsset: "usd",
// ConversionFactor: i(6),
// DebtFloor: i(10000000),
// }
// testDPUpdatedDebtFloor := testDP
// testDPUpdatedDebtFloor.DebtFloor = i(1000)
// // cdp Genesis
// testCDPParams := cdptypes.DefaultParams()
// testCDPParams.CollateralParams = testCPs
// testCDPParams.DebtParam = testDP
// testCDPParams.GlobalDebtLimit = testCPs[0].DebtLimit.Add(testCPs[0].DebtLimit) // correct global debt limit to pass genesis validation
// testDeputy, err := sdk.AccAddressFromBech32("0g1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj")
// suite.Require().NoError(err)
// // bep3 Asset Params
// testAPs := bep3types.AssetParams{
// bep3types.AssetParam{
// Denom: "bnb",
// CoinID: 714,
// SupplyLimit: bep3types.SupplyLimit{
// Limit: sdkmath.NewInt(350000000000000),
// TimeLimited: false,
// TimeBasedLimit: sdk.ZeroInt(),
// TimePeriod: time.Hour,
// },
// Active: true,
// DeputyAddress: testDeputy,
// FixedFee: sdkmath.NewInt(1000),
// MinSwapAmount: sdk.OneInt(),
// MaxSwapAmount: sdkmath.NewInt(1000000000000),
// MinBlockLock: bep3types.DefaultMinBlockLock,
// MaxBlockLock: bep3types.DefaultMaxBlockLock,
// },
// bep3types.AssetParam{
// Denom: "inc",
// CoinID: 9999,
// SupplyLimit: bep3types.SupplyLimit{
// Limit: sdkmath.NewInt(100000000000000),
// TimeLimited: true,
// TimeBasedLimit: sdkmath.NewInt(50000000000),
// TimePeriod: time.Hour,
// },
// Active: false,
// DeputyAddress: testDeputy,
// FixedFee: sdkmath.NewInt(1000),
// MinSwapAmount: sdk.OneInt(),
// MaxSwapAmount: sdkmath.NewInt(1000000000000),
// MinBlockLock: bep3types.DefaultMinBlockLock,
// MaxBlockLock: bep3types.DefaultMaxBlockLock,
// },
// }
// testAPsUpdatedActive := make(bep3types.AssetParams, len(testAPs))
// copy(testAPsUpdatedActive, testAPs)
// testAPsUpdatedActive[1].Active = true
// // bep3 Genesis
// testBep3Params := bep3types.DefaultParams()
// testBep3Params.AssetParams = testAPs
// // pricefeed Markets
// testMs := pricefeedtypes.Markets{
// {
// MarketID: "bnb:usd",
// BaseAsset: "bnb",
// QuoteAsset: "usd",
// Oracles: []sdk.AccAddress{},
// Active: true,
// },
// {
// MarketID: "btc:usd",
// BaseAsset: "btc",
// QuoteAsset: "usd",
// Oracles: []sdk.AccAddress{},
// Active: true,
// },
// }
// testMsUpdatedActive := make(pricefeedtypes.Markets, len(testMs))
// copy(testMsUpdatedActive, testMs)
// testMsUpdatedActive[1].Active = true
// testcases := []struct {
// name string
// genState []app.GenesisState
// permission types.SubParamChangePermission
// pubProposal types.PubProposal
// expectAllowed bool
// }{
// {
// name: "normal",
// genState: []app.GenesisState{
// newPricefeedGenState([]string{"bnb", "btc"}, []sdk.Dec{d("15.01"), d("9500")}),
// newCDPGenesisState(testCDPParams),
// newBep3GenesisState(testBep3Params),
// },
// permission: types.SubParamChangePermission{
// AllowedParams: types.AllowedParams{
// {Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyDebtThreshold)},
// {Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyCollateralParams)},
// {Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyDebtParam)},
// {Subspace: bep3types.ModuleName, Key: string(bep3types.KeyAssetParams)},
// {Subspace: pricefeedtypes.ModuleName, Key: string(pricefeedtypes.KeyMarkets)},
// },
// AllowedCollateralParams: types.AllowedCollateralParams{
// {
// Type: "bnb-a",
// DebtLimit: true,
// StabilityFee: true,
// },
// { // TODO currently even if a perm doesn't allow a change in one element it must still be present in list
// Type: "btc-a",
// },
// },
// AllowedDebtParam: types.AllowedDebtParam{
// DebtFloor: true,
// },
// AllowedAssetParams: types.AllowedAssetParams{
// {
// Denom: "bnb",
// },
// {
// Denom: "inc",
// Active: true,
// },
// },
// AllowedMarkets: types.AllowedMarkets{
// {
// MarketID: "bnb:usd",
// },
// {
// MarketID: "btc:usd",
// Active: true,
// },
// },
// },
// pubProposal: paramstypes.NewParameterChangeProposal(
// "A Title",
// "A description for this proposal.",
// []paramstypes.ParamChange{
// {
// Subspace: cdptypes.ModuleName,
// Key: string(cdptypes.KeyDebtThreshold),
// Value: string(suite.cdc.MustMarshalJSON(i(1234))),
// },
// {
// Subspace: cdptypes.ModuleName,
// Key: string(cdptypes.KeyCollateralParams),
// Value: string(suite.cdc.MustMarshalJSON(testCPUpdatedDebtLimit)),
// },
// {
// Subspace: cdptypes.ModuleName,
// Key: string(cdptypes.KeyDebtParam),
// Value: string(suite.cdc.MustMarshalJSON(testDPUpdatedDebtFloor)),
// },
// {
// Subspace: bep3types.ModuleName,
// Key: string(bep3types.KeyAssetParams),
// Value: string(suite.cdc.MustMarshalJSON(testAPsUpdatedActive)),
// },
// {
// Subspace: pricefeedtypes.ModuleName,
// Key: string(pricefeedtypes.KeyMarkets),
// Value: string(suite.cdc.MustMarshalJSON(testMsUpdatedActive)),
// },
// },
// ),
// expectAllowed: true,
// },
// {
// name: "not allowed (wrong pubproposal type)",
// permission: types.SubParamChangePermission{},
// pubProposal: govtypes.NewTextProposal("A Title", "A description for this proposal."),
// expectAllowed: false,
// },
// {
// name: "not allowed (nil pubproposal)",
// permission: types.SubParamChangePermission{},
// pubProposal: nil,
// expectAllowed: false,
// },
// // TODO more cases
// }
// for _, tc := range testcases {
// suite.Run(tc.name, func() {
// tApp := app.NewTestApp()
// ctx := tApp.NewContext(true, abci.Header{})
// tApp.InitializeFromGenesisStates(tc.genState...)
// suite.Equal(
// tc.expectAllowed,
// tc.permission.Allows(ctx, tApp.Codec(), tApp.GetParamsKeeper(), tc.pubProposal),
// )
// })
// }
// }
func TestPermissionTestSuite(t *testing.T) {
suite.Run(t, new(PermissionTestSuite))
}

View File

@ -0,0 +1,189 @@
package keeper_test
// import (
// "testing"
// "time"
// "github.com/stretchr/testify/suite"
// abci "github.com/cometbft/cometbft/abci/types"
// govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
// paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
// "github.com/0glabs/0g-chain/app"
// "github.com/0glabs/0g-chain/x/committee/types"
// )
// type TypesTestSuite struct {
// suite.Suite
// }
// func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
// testcases := []struct {
// name string
// permissions []types.Permission
// pubProposal types.PubProposal
// expectHasPermissions bool
// }{
// {
// name: "normal (single permission)",
// permissions: []types.Permission{types.SimpleParamChangePermission{
// AllowedParams: types.AllowedParams{
// {
// Subspace: "cdp",
// Key: "DebtThreshold",
// },
// }}},
// pubProposal: paramstypes.NewParameterChangeProposal(
// "A Title",
// "A description of this proposal.",
// []paramstypes.ParamChange{
// {
// Subspace: "cdp",
// Key: "DebtThreshold",
// Value: `{"denom": "usdx", "amount": "1000000"}`,
// },
// },
// ),
// expectHasPermissions: true,
// },
// {
// name: "normal (multiple permissions)",
// permissions: []types.Permission{
// types.SimpleParamChangePermission{
// AllowedParams: types.AllowedParams{
// {
// Subspace: "cdp",
// Key: "DebtThreshold",
// },
// }},
// types.TextPermission{},
// },
// pubProposal: govtypes.NewTextProposal("A Proposal Title", "A description of this proposal"),
// expectHasPermissions: true,
// },
// {
// name: "overruling permission",
// permissions: []types.Permission{
// types.SimpleParamChangePermission{
// AllowedParams: types.AllowedParams{
// {
// Subspace: "cdp",
// Key: "DebtThreshold",
// },
// }},
// types.GodPermission{},
// },
// pubProposal: paramstypes.NewParameterChangeProposal(
// "A Title",
// "A description of this proposal.",
// []paramstypes.ParamChange{
// {
// Subspace: "cdp",
// Key: "CollateralParams",
// Value: `[]`,
// },
// },
// ),
// expectHasPermissions: true,
// },
// {
// name: "no permissions",
// permissions: nil,
// pubProposal: paramstypes.NewParameterChangeProposal(
// "A Title",
// "A description of this proposal.",
// []paramstypes.ParamChange{
// {
// Subspace: "cdp",
// Key: "CollateralParams",
// Value: `[]`,
// },
// },
// ),
// expectHasPermissions: false,
// },
// {
// name: "split permissions",
// // These permissions looks like they allow the param change proposal, however a proposal must pass a single permission independently of others.
// permissions: []types.Permission{
// types.SimpleParamChangePermission{
// AllowedParams: types.AllowedParams{
// {
// Subspace: "cdp",
// Key: "DebtThreshold",
// },
// }},
// types.SimpleParamChangePermission{
// AllowedParams: types.AllowedParams{
// {
// Subspace: "cdp",
// Key: "DebtParams",
// },
// }},
// },
// pubProposal: paramstypes.NewParameterChangeProposal(
// "A Title",
// "A description of this proposal.",
// []paramstypes.ParamChange{
// {
// Subspace: "cdp",
// Key: "DebtThreshold",
// Value: `{"denom": "usdx", "amount": "1000000"}`,
// },
// {
// Subspace: "cdp",
// Key: "DebtParams",
// Value: `[]`,
// },
// },
// ),
// expectHasPermissions: false,
// },
// {
// name: "unregistered proposal",
// permissions: []types.Permission{
// types.SimpleParamChangePermission{
// AllowedParams: types.AllowedParams{
// {
// Subspace: "cdp",
// Key: "DebtThreshold",
// },
// }},
// },
// pubProposal: UnregisteredPubProposal{govtypes.TextProposal{Title: "A Title", Description: "A description."}},
// expectHasPermissions: false,
// },
// }
// for _, tc := range testcases {
// suite.Run(tc.name, func() {
// tApp := app.NewTestApp()
// ctx := tApp.NewContext(true, abci.Header{})
// tApp.InitializeFromGenesisStates()
// com := types.NewMemberCommittee(
// 12,
// "a description of this committee",
// nil,
// tc.permissions,
// d("0.5"),
// 24*time.Hour,
// types.FirstPastThePost,
// )
// suite.Equal(
// tc.expectHasPermissions,
// com.HasPermissionsFor(ctx, tApp.Codec(), tApp.GetParamsKeeper(), tc.pubProposal),
// )
// })
// }
// }
// func TestTypesTestSuite(t *testing.T) {
// suite.Run(t, new(TypesTestSuite))
// }

View File

@ -0,0 +1,53 @@
package keeper_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"github.com/0glabs/0g-chain/x/committee/testutil"
"github.com/0glabs/0g-chain/x/committee/types"
)
type grpcQueryTestSuite struct {
testutil.Suite
}
func (suite *grpcQueryTestSuite) SetupTest() {
suite.Suite.SetupTest()
}
func (suite *grpcQueryTestSuite) TestVote() {
ctx, keeper, queryClient := suite.Ctx, suite.Keeper, suite.QueryClient
vote := types.Vote{
ProposalID: 1,
Voter: suite.Addresses[0],
VoteType: types.VOTE_TYPE_ABSTAIN,
}
keeper.SetVote(ctx, vote)
req := types.QueryVoteRequest{
ProposalId: vote.ProposalID,
Voter: vote.Voter.String(),
}
res, err := queryClient.Vote(context.Background(), &req)
suite.Require().NoError(err)
suite.Require().Equal(vote.ProposalID, res.ProposalID)
suite.Require().Equal(vote.VoteType, res.VoteType)
suite.Require().Equal(vote.Voter.String(), res.Voter)
queryRes, err := queryClient.Votes(context.Background(), &types.QueryVotesRequest{
ProposalId: vote.ProposalID,
})
suite.Require().NoError(err)
suite.Require().Len(queryRes.Votes, 1)
suite.Require().Equal(vote.ProposalID, queryRes.Votes[0].ProposalID)
suite.Require().Equal(vote.VoteType, queryRes.Votes[0].VoteType)
suite.Require().Equal(vote.Voter.String(), queryRes.Votes[0].Voter)
}
func TestGrpcQueryTestSuite(t *testing.T) {
suite.Run(t, new(grpcQueryTestSuite))
}

View File

@ -0,0 +1,204 @@
package keeper
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/0glabs/0g-chain/x/committee/types"
)
type queryServer struct {
keeper Keeper
}
// NewQueryServerImpl creates a new server for handling gRPC queries.
func NewQueryServerImpl(k Keeper) types.QueryServer {
return &queryServer{keeper: k}
}
var _ types.QueryServer = queryServer{}
// Committees implements the gRPC service handler for querying committees.
func (s queryServer) Committees(ctx context.Context, req *types.QueryCommitteesRequest) (*types.QueryCommitteesResponse, error) {
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "empty request")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
committees := s.keeper.GetCommittees(sdkCtx)
committeesAny, err := types.PackCommittees(committees)
if err != nil {
return nil, status.Errorf(codes.Unknown, "could not pack committees: %v", err)
}
return &types.QueryCommitteesResponse{Committees: committeesAny}, nil
}
// Committee implements the Query/Committee gRPC method.
func (s queryServer) Committee(c context.Context, req *types.QueryCommitteeRequest) (*types.QueryCommitteeResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
committee, found := s.keeper.GetCommittee(ctx, req.CommitteeId)
if !found {
return nil, status.Errorf(codes.NotFound, "could not find committee for id: %v", req.CommitteeId)
}
committeeAny, err := types.PackCommittee(committee)
if err != nil {
return nil, status.Errorf(codes.Internal, "could not pack committees: %v", err)
}
return &types.QueryCommitteeResponse{Committee: committeeAny}, nil
}
// Proposals implements the Query/Proposals gRPC method
func (s queryServer) Proposals(c context.Context, req *types.QueryProposalsRequest) (*types.QueryProposalsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
proposals := s.keeper.GetProposalsByCommittee(ctx, req.CommitteeId)
var proposalsResp []types.QueryProposalResponse
for _, proposal := range proposals {
proposalsResp = append(proposalsResp, s.proposalResponseFromProposal(proposal))
}
return &types.QueryProposalsResponse{
Proposals: proposalsResp,
}, nil
}
// Proposal implements the Query/Proposal gRPC method
func (s queryServer) Proposal(c context.Context, req *types.QueryProposalRequest) (*types.QueryProposalResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
proposal, found := s.keeper.GetProposal(ctx, req.ProposalId)
if !found {
return nil, status.Errorf(codes.NotFound, "cannot find proposal: %v", req.ProposalId)
}
proposalResp := s.proposalResponseFromProposal(proposal)
return &proposalResp, nil
}
// NextProposalID implements the Query/NextProposalID gRPC method
func (s queryServer) NextProposalID(c context.Context, req *types.QueryNextProposalIDRequest) (*types.QueryNextProposalIDResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
proposalID, err := s.keeper.GetNextProposalID(ctx)
if err != nil {
return nil, status.Errorf(codes.NotFound, "cannot find next proposal id: %v", err)
}
return &types.QueryNextProposalIDResponse{NextProposalID: proposalID}, nil
}
// Votes implements the Query/Votes gRPC method
func (s queryServer) Votes(c context.Context, req *types.QueryVotesRequest) (*types.QueryVotesResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
var queryResults []types.QueryVoteResponse
store := ctx.KVStore(s.keeper.storeKey)
votesStore := prefix.NewStore(store, append(types.VoteKeyPrefix, types.GetKeyFromID(req.ProposalId)...))
pageRes, err := query.Paginate(votesStore, req.Pagination, func(key []byte, value []byte) error {
var vote types.Vote
if err := s.keeper.cdc.Unmarshal(value, &vote); err != nil {
return err
}
queryResults = append(queryResults, s.votesResponseFromVote(vote))
return nil
})
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &types.QueryVotesResponse{
Votes: queryResults,
Pagination: pageRes,
}, nil
}
// Vote implements the Query/Vote gRPC method
func (s queryServer) Vote(c context.Context, req *types.QueryVoteRequest) (*types.QueryVoteResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
voter, err := sdk.AccAddressFromBech32(req.Voter)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid voter address: %v", err)
}
vote, found := s.keeper.GetVote(ctx, req.ProposalId, voter)
if !found {
return nil, status.Errorf(codes.NotFound, "proposal id: %d, voter: %s", req.ProposalId, req.Voter)
}
voteResp := s.votesResponseFromVote(vote)
return &voteResp, nil
}
// Tally implements the Query/Tally gRPC method
func (s queryServer) Tally(c context.Context, req *types.QueryTallyRequest) (*types.QueryTallyResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
tally, found := s.keeper.GetProposalTallyResponse(ctx, req.ProposalId)
if !found {
return nil, status.Errorf(codes.NotFound, "proposal id: %d", req.ProposalId)
}
return tally, nil
}
// RawParams implements the Query/RawParams gRPC method
func (s queryServer) RawParams(c context.Context, req *types.QueryRawParamsRequest) (*types.QueryRawParamsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(c)
subspace, found := s.keeper.paramKeeper.GetSubspace(req.Subspace)
if !found {
return nil, status.Errorf(codes.NotFound, "subspace not found: %s", req.Subspace)
}
rawParams := subspace.GetRaw(ctx, []byte(req.Key))
return &types.QueryRawParamsResponse{RawData: string(rawParams)}, nil
}
func (s queryServer) proposalResponseFromProposal(proposal types.Proposal) types.QueryProposalResponse {
return types.QueryProposalResponse{
PubProposal: proposal.Content,
ID: proposal.ID,
CommitteeID: proposal.CommitteeID,
Deadline: proposal.Deadline,
}
}
func (s queryServer) votesResponseFromVote(vote types.Vote) types.QueryVoteResponse {
return types.QueryVoteResponse{
ProposalID: vote.ProposalID,
Voter: vote.Voter.String(),
VoteType: vote.VoteType,
}
}

View File

@ -0,0 +1,64 @@
package keeper_test
import (
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/testutil"
"github.com/0glabs/0g-chain/x/committee/types"
)
// getProposalVoteMap collects up votes into a map indexed by proposalID
func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vote) {
proposalVoteMap := map[uint64]([]types.Vote){}
k.IterateProposals(ctx, func(p types.Proposal) bool {
proposalVoteMap[p.ID] = k.GetVotesByProposal(ctx, p.ID)
return false
})
return proposalVoteMap
}
func (suite *keeperTestSuite) getAccount(addr sdk.AccAddress) authtypes.AccountI {
ak := suite.App.GetAccountKeeper()
return ak.GetAccount(suite.Ctx, addr)
}
func mustNewTestMemberCommittee(addresses []sdk.AccAddress) *types.MemberCommittee {
com, err := types.NewMemberCommittee(
12,
"This committee is for testing.",
addresses,
[]types.Permission{&types.GodPermission{}},
testutil.D("0.667"),
time.Hour*24*7,
types.TALLY_OPTION_FIRST_PAST_THE_POST,
)
if err != nil {
panic(err)
}
return com
}
// mustNewTestProposal returns a new test proposal.
func mustNewTestProposal() types.Proposal {
proposal, err := types.NewProposal(
govv1beta1.NewTextProposal("A Title", "A description of this proposal."),
1, 1, time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC),
)
if err != nil {
panic(err)
}
return proposal
}
// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps.
func NewCommitteeGenesisState(cdc codec.Codec, gs *types.GenesisState) app.GenesisState {
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(gs)}
}

View File

@ -0,0 +1,301 @@
package keeper
import (
"time"
errorsmod "cosmossdk.io/errors"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"github.com/0glabs/0g-chain/x/committee/types"
)
type Keeper struct {
cdc codec.Codec
storeKey storetypes.StoreKey
paramKeeper types.ParamKeeper
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
// Proposal router
router govv1beta1.Router
}
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, router govv1beta1.Router,
paramKeeper types.ParamKeeper, ak types.AccountKeeper, sk types.BankKeeper,
) Keeper {
// Logic in the keeper methods assume the set of gov handlers is fixed.
// So the gov router must be sealed so no handlers can be added or removed after the keeper is created.
router.Seal()
return Keeper{
cdc: cdc,
storeKey: storeKey,
paramKeeper: paramKeeper,
accountKeeper: ak,
bankKeeper: sk,
router: router,
}
}
// ------------------------------------------
// Committees
// ------------------------------------------
// GetCommittee gets a committee from the store.
func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) {
var committee types.Committee
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
bz := store.Get(types.GetKeyFromID(committeeID))
if bz == nil {
return committee, false
}
err := k.cdc.UnmarshalInterface(bz, &committee)
if err != nil {
panic(err)
}
return committee, true
}
// SetCommittee puts a committee into the store.
func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
bz, err := k.cdc.MarshalInterface(committee)
if err != nil {
panic(err)
}
store.Set(types.GetKeyFromID(committee.GetID()), bz)
}
// DeleteCommittee removes a committee from the store.
func (k Keeper) DeleteCommittee(ctx sdk.Context, committeeID uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
store.Delete(types.GetKeyFromID(committeeID))
}
// IterateCommittees provides an iterator over all stored committees.
// For each committee, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateCommittees(ctx sdk.Context, cb func(committee types.Committee) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var committee types.Committee
if err := k.cdc.UnmarshalInterface(iterator.Value(), &committee); err != nil {
panic(err)
}
if cb(committee) {
break
}
}
}
// GetCommittees returns all stored committees.
func (k Keeper) GetCommittees(ctx sdk.Context) types.Committees {
results := types.Committees{}
k.IterateCommittees(ctx, func(com types.Committee) bool {
results = append(results, com)
return false
})
return results
}
// ------------------------------------------
// Proposals
// ------------------------------------------
// SetNextProposalID stores an ID to be used for the next created proposal
func (k Keeper) SetNextProposalID(ctx sdk.Context, id uint64) {
store := ctx.KVStore(k.storeKey)
store.Set(types.NextProposalIDKey, types.GetKeyFromID(id))
}
// GetNextProposalID reads the next available global ID from store
func (k Keeper) GetNextProposalID(ctx sdk.Context) (uint64, error) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.NextProposalIDKey)
if bz == nil {
return 0, errorsmod.Wrap(types.ErrInvalidGenesis, "next proposal ID not set at genesis")
}
return types.Uint64FromBytes(bz), nil
}
// IncrementNextProposalID increments the next proposal ID in the store by 1.
func (k Keeper) IncrementNextProposalID(ctx sdk.Context) error {
id, err := k.GetNextProposalID(ctx)
if err != nil {
return err
}
k.SetNextProposalID(ctx, id+1)
return nil
}
// StoreNewProposal stores a proposal, adding a new ID
func (k Keeper) StoreNewProposal(ctx sdk.Context, pubProposal types.PubProposal, committeeID uint64, deadline time.Time) (uint64, error) {
newProposalID, err := k.GetNextProposalID(ctx)
if err != nil {
return 0, err
}
proposal, err := types.NewProposal(
pubProposal,
newProposalID,
committeeID,
deadline,
)
if err != nil {
return 0, err
}
k.SetProposal(ctx, proposal)
err = k.IncrementNextProposalID(ctx)
if err != nil {
return 0, err
}
return newProposalID, nil
}
// GetProposal gets a proposal from the store.
func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
bz := store.Get(types.GetKeyFromID(proposalID))
if bz == nil {
return types.Proposal{}, false
}
var proposal types.Proposal
k.cdc.MustUnmarshal(bz, &proposal)
return proposal, true
}
// SetProposal puts a proposal into the store.
func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
bz := k.cdc.MustMarshal(&proposal)
store.Set(types.GetKeyFromID(proposal.ID), bz)
}
// DeleteProposal removes a proposal from the store.
func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
store.Delete(types.GetKeyFromID(proposalID))
}
// IterateProposals provides an iterator over all stored proposals.
// For each proposal, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateProposals(ctx sdk.Context, cb func(proposal types.Proposal) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var proposal types.Proposal
k.cdc.MustUnmarshal(iterator.Value(), &proposal)
if cb(proposal) {
break
}
}
}
// GetProposals returns all stored proposals.
func (k Keeper) GetProposals(ctx sdk.Context) types.Proposals {
results := types.Proposals{}
k.IterateProposals(ctx, func(prop types.Proposal) bool {
results = append(results, prop)
return false
})
return results
}
// GetProposalsByCommittee returns all proposals for one committee.
func (k Keeper) GetProposalsByCommittee(ctx sdk.Context, committeeID uint64) types.Proposals {
results := types.Proposals{}
k.IterateProposals(ctx, func(prop types.Proposal) bool {
if prop.CommitteeID == committeeID {
results = append(results, prop)
}
return false
})
return results
}
// DeleteProposalAndVotes removes a proposal and its associated votes.
func (k Keeper) DeleteProposalAndVotes(ctx sdk.Context, proposalID uint64) {
votes := k.GetVotesByProposal(ctx, proposalID)
k.DeleteProposal(ctx, proposalID)
for _, v := range votes {
k.DeleteVote(ctx, v.ProposalID, v.Voter)
}
}
// ------------------------------------------
// Votes
// ------------------------------------------
// GetVote gets a vote from the store.
func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
bz := store.Get(types.GetVoteKey(proposalID, voter))
if bz == nil {
return types.Vote{}, false
}
var vote types.Vote
k.cdc.MustUnmarshal(bz, &vote)
return vote, true
}
// SetVote puts a vote into the store.
func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
bz := k.cdc.MustMarshal(&vote)
store.Set(types.GetVoteKey(vote.ProposalID, vote.Voter), bz)
}
// DeleteVote removes a Vote from the store.
func (k Keeper) DeleteVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
store.Delete(types.GetVoteKey(proposalID, voter))
}
// IterateVotes provides an iterator over all stored votes.
// For each vote, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateVotes(ctx sdk.Context, cb func(vote types.Vote) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var vote types.Vote
k.cdc.MustUnmarshal(iterator.Value(), &vote)
if cb(vote) {
break
}
}
}
// GetVotes returns all stored votes.
func (k Keeper) GetVotes(ctx sdk.Context) []types.Vote {
results := []types.Vote{}
k.IterateVotes(ctx, func(vote types.Vote) bool {
results = append(results, vote)
return false
})
return results
}
// GetVotesByProposal returns all votes for one proposal.
func (k Keeper) GetVotesByProposal(ctx sdk.Context, proposalID uint64) []types.Vote {
results := []types.Vote{}
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), append(types.VoteKeyPrefix, types.GetKeyFromID(proposalID)...))
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var vote types.Vote
k.cdc.MustUnmarshal(iterator.Value(), &vote)
results = append(results, vote)
}
return results
}

View File

@ -0,0 +1,171 @@
package keeper_test
import (
"testing"
"time"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"github.com/stretchr/testify/suite"
"github.com/0glabs/0g-chain/x/committee/testutil"
"github.com/0glabs/0g-chain/x/committee/types"
)
type keeperTestSuite struct {
testutil.Suite
}
func (suite *keeperTestSuite) SetupTest() {
suite.Suite.SetupTest()
}
func (suite *keeperTestSuite) TestGetSetDeleteCommittee() {
cdc := suite.App.AppCodec()
// setup test
com := mustNewTestMemberCommittee(suite.Addresses)
// write and read from store
suite.Keeper.SetCommittee(suite.Ctx, com)
readCommittee, found := suite.Keeper.GetCommittee(suite.Ctx, com.ID)
// check before and after match
suite.Require().True(found)
expectedJson, err := cdc.MarshalJSON(com)
suite.Require().NoError(err)
actualJson, err := cdc.MarshalJSON(readCommittee)
suite.Require().NoError(err)
suite.Equal(expectedJson, actualJson)
suite.Require().Equal(com.GetPermissions(), readCommittee.GetPermissions())
// delete from store
suite.Keeper.DeleteCommittee(suite.Ctx, com.ID)
// check does not exist
_, found = suite.Keeper.GetCommittee(suite.Ctx, com.ID)
suite.Require().False(found)
}
func (suite *keeperTestSuite) TestGetSetDeleteProposal() {
// test setup
prop, err := types.NewProposal(
govv1beta1.NewTextProposal("A Title", "A description of this proposal."),
12,
0,
time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC),
)
suite.Require().NoError(err)
// write and read from store
suite.Keeper.SetProposal(suite.Ctx, prop)
readProposal, found := suite.Keeper.GetProposal(suite.Ctx, prop.ID)
// check before and after match
suite.True(found)
suite.Equal(prop, readProposal)
// delete from store
suite.Keeper.DeleteProposal(suite.Ctx, prop.ID)
// check does not exist
_, found = suite.Keeper.GetProposal(suite.Ctx, prop.ID)
suite.False(found)
}
func (suite *keeperTestSuite) TestGetSetDeleteVote() {
// test setup
vote := types.Vote{
ProposalID: 12,
Voter: suite.Addresses[0],
}
// write and read from store
suite.Keeper.SetVote(suite.Ctx, vote)
readVote, found := suite.Keeper.GetVote(suite.Ctx, vote.ProposalID, vote.Voter)
// check before and after match
suite.True(found)
suite.Equal(vote, readVote)
// delete from store
suite.Keeper.DeleteVote(suite.Ctx, vote.ProposalID, vote.Voter)
// check does not exist
_, found = suite.Keeper.GetVote(suite.Ctx, vote.ProposalID, vote.Voter)
suite.False(found)
}
func (suite *keeperTestSuite) TestGetCommittees() {
committeesCount := 10
for i := 0; i < committeesCount; i++ {
com := mustNewTestMemberCommittee(suite.Addresses)
com.ID = uint64(i)
suite.Keeper.SetCommittee(suite.Ctx, com)
}
committees := suite.Keeper.GetCommittees(suite.Ctx)
suite.Require().Len(committees, committeesCount)
}
func (suite *keeperTestSuite) TestGetAndSetProposal() {
proposal := mustNewTestProposal()
// Get no proposal
actualProposal, found := suite.Keeper.GetProposal(suite.Ctx, proposal.ID)
suite.Require().False(found)
suite.Require().Equal(types.Proposal{}, actualProposal)
// Set and get new proposal
suite.Keeper.SetProposal(suite.Ctx, proposal)
actualProposal, found = suite.Keeper.GetProposal(suite.Ctx, proposal.ID)
suite.Require().True(found)
suite.Require().Equal(proposal, actualProposal)
}
func (suite *keeperTestSuite) TestGetProposalsByCommittee() {
committee := mustNewTestMemberCommittee(suite.Addresses)
proposalsCount := 4
for i := 0; i < proposalsCount; i++ {
proposal := mustNewTestProposal()
proposal.ID = uint64(i)
proposal.CommitteeID = committee.ID
suite.Keeper.SetProposal(suite.Ctx, proposal)
}
proposal := mustNewTestProposal()
proposal.ID = uint64(proposalsCount)
proposal.CommitteeID = committee.ID + 1
suite.Keeper.SetProposal(suite.Ctx, proposal)
// No proposals
actualProposals := suite.Keeper.GetProposalsByCommittee(suite.Ctx, committee.ID+2)
suite.Require().Len(actualProposals, 0)
// Proposals for existing committees
actualProposals = suite.Keeper.GetProposalsByCommittee(suite.Ctx, committee.ID)
suite.Require().Len(actualProposals, proposalsCount)
actualProposals = suite.Keeper.GetProposalsByCommittee(suite.Ctx, committee.ID+1)
suite.Require().Len(actualProposals, 1)
// Make sure proposals have expected data
suite.Require().Equal(proposal, actualProposals[0])
}
func (suite *keeperTestSuite) TestGetVotesByProposal() {
proposal := mustNewTestProposal()
suite.Keeper.SetProposal(suite.Ctx, proposal)
votes := []types.Vote{
types.NewVote(proposal.ID, suite.Addresses[0], types.VOTE_TYPE_NO),
types.NewVote(proposal.ID, suite.Addresses[1], types.VOTE_TYPE_ABSTAIN),
types.NewVote(proposal.ID, suite.Addresses[1], types.VOTE_TYPE_YES),
}
expectedVotes := []types.Vote{votes[0], votes[2]}
for _, vote := range votes {
suite.Keeper.SetVote(suite.Ctx, vote)
}
actualVotes := suite.Keeper.GetVotesByProposal(suite.Ctx, proposal.ID)
suite.Require().Len(actualVotes, len(expectedVotes))
suite.Require().ElementsMatch(expectedVotes, actualVotes)
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(keeperTestSuite))
}

View File

@ -0,0 +1,70 @@
package keeper
import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/0glabs/0g-chain/x/committee/types"
)
type msgServer struct {
keeper Keeper
}
// NewMsgServerImpl returns an implementation of the committee MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
return &msgServer{keeper: keeper}
}
var _ types.MsgServer = msgServer{}
// SubmitProposal handles MsgSubmitProposal messages
func (m msgServer) SubmitProposal(goCtx context.Context, msg *types.MsgSubmitProposal) (*types.MsgSubmitProposalResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
proposer, err := sdk.AccAddressFromBech32(msg.Proposer)
if err != nil {
return nil, err
}
proposalID, err := m.keeper.SubmitProposal(ctx, proposer, msg.CommitteeID, msg.GetPubProposal())
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Proposer),
),
)
return &types.MsgSubmitProposalResponse{ProposalID: proposalID}, nil
}
// Vote handles MsgVote messages
func (m msgServer) Vote(goCtx context.Context, msg *types.MsgVote) (*types.MsgVoteResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
voter, err := sdk.AccAddressFromBech32(msg.Voter)
if err != nil {
return nil, err
}
if err := m.keeper.AddVote(ctx, msg.ProposalID, voter, msg.VoteType); err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Voter),
),
)
return &types.MsgVoteResponse{}, nil
}

View File

@ -0,0 +1,194 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdkmath "cosmossdk.io/math"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
"github.com/0glabs/0g-chain/app"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/types"
// swaptypes "github.com/0glabs/0g-chain/x/swap/types"
)
//NewDistributionGenesisWithPool creates a default distribution genesis state with some coins in the community pool.
//func NewDistributionGenesisWithPool(communityPoolCoins sdk.Coins) app.GenesisState {
//gs := distribution.DefaultGenesisState()
//gs.FeePool = distribution.FeePool{CommunityPool: sdk.NewDecCoinsFromCoins(communityPoolCoins...)}
//return app.GenesisState{distribution.ModuleName: distribution.ModuleCdc.MustMarshalJSON(gs)}
//}
type MsgServerTestSuite struct {
suite.Suite
app app.TestApp
keeper keeper.Keeper
msgServer types.MsgServer
ctx sdk.Context
addresses []sdk.AccAddress
communityPoolAmt sdk.Coins
}
func (suite *MsgServerTestSuite) SetupTest() {
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.msgServer = keeper.NewMsgServerImpl(suite.keeper)
encodingCfg := app.MakeEncodingConfig()
cdc := encodingCfg.Marshaler
memberCommittee, err := types.NewMemberCommittee(
1,
"This committee is for testing.",
suite.addresses[:3],
[]types.Permission{&types.GodPermission{}},
sdk.MustNewDecFromStr("0.5"),
time.Hour*24*7,
types.TALLY_OPTION_FIRST_PAST_THE_POST,
)
suite.Require().NoError(err)
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
testGenesis := types.NewGenesisState(
3,
[]types.Committee{memberCommittee},
[]types.Proposal{},
[]types.Vote{},
)
suite.communityPoolAmt = sdk.NewCoins(sdk.NewCoin("neuron", sdkmath.NewInt(1000000000000000)))
suite.app.InitializeFromGenesisStates(
app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(testGenesis)},
// TODO: not used?
// NewDistributionGenesisWithPool(suite.communityPoolAmt),
)
suite.ctx = suite.app.NewContext(true, tmproto.Header{Height: 1, Time: firstBlockTime})
}
// func (suite *MsgServerTestSuite) TestSubmitProposalMsg_Valid() {
// msg, err := types.NewMsgSubmitProposal(
// proposal.NewParameterChangeProposal(
// "A Title",
// "A description of this proposal.",
// []proposal.ParamChange{{
// Subspace: swaptypes.ModuleName,
// Key: string(swaptypes.KeySwapFee),
// Value: "\"0.001500000000000000\"",
// }},
// ),
// suite.addresses[0],
// 1,
// )
// suite.Require().NoError(err)
// res, err := suite.msgServer.SubmitProposal(sdk.WrapSDKContext(suite.ctx), msg)
// suite.NoError(err)
// _, found := suite.keeper.GetProposal(suite.ctx, res.ProposalID)
// suite.True(found)
// }
// func (suite *MsgServerTestSuite) TestSubmitProposalMsg_Invalid() {
// var committeeID uint64 = 1
// msg, err := types.NewMsgSubmitProposal(
// proposal.NewParameterChangeProposal(
// "A Title",
// "A description of this proposal.",
// []proposal.ParamChange{{
// Subspace: swaptypes.ModuleName,
// Key: "nonsense-key",
// Value: "nonsense-value",
// }},
// ),
// suite.addresses[0],
// committeeID,
// )
// suite.Require().NoError(err)
// _, err = suite.msgServer.SubmitProposal(sdk.WrapSDKContext(suite.ctx), msg)
// suite.Error(err)
// suite.Empty(
// suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID),
// "proposal found when none should exist",
// )
// }
func (suite *MsgServerTestSuite) TestSubmitProposalMsg_ValidUpgrade() {
msg, err := types.NewMsgSubmitProposal(
upgradetypes.NewSoftwareUpgradeProposal(
"A Title",
"A description of this proposal.",
upgradetypes.Plan{
Name: "emergency-shutdown-1", // identifier for the upgrade
Height: 100000,
Info: "Some information about the shutdown.",
},
),
suite.addresses[0],
1,
)
suite.Require().NoError(err)
res, err := suite.msgServer.SubmitProposal(sdk.WrapSDKContext(suite.ctx), msg)
suite.NoError(err)
_, found := suite.keeper.GetProposal(suite.ctx, res.ProposalID)
suite.True(found)
}
// TODO: create a unregisted proto for tests?
func (suite *MsgServerTestSuite) TestSubmitProposalMsg_Unregistered() {
var committeeID uint64 = 1
msg, err := types.NewMsgSubmitProposal(
&UnregisteredPubProposal{},
suite.addresses[0],
committeeID,
)
suite.Require().NoError(err)
_, err = suite.msgServer.SubmitProposal(sdk.WrapSDKContext(suite.ctx), msg)
suite.Error(err)
suite.Empty(
suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID),
"proposal found when none should exist",
)
}
// func (suite *MsgServerTestSuite) TestSubmitProposalMsgAndVote() {
// msg, err := types.NewMsgSubmitProposal(
// proposal.NewParameterChangeProposal(
// "A Title",
// "A description of this proposal.",
// []proposal.ParamChange{{
// Subspace: swaptypes.ModuleName,
// Key: string(swaptypes.KeySwapFee),
// Value: "\"0.001500000000000000\"",
// }},
// ),
// suite.addresses[0],
// 1,
// )
// suite.Require().NoError(err)
// res, err := suite.msgServer.SubmitProposal(sdk.WrapSDKContext(suite.ctx), msg)
// suite.Require().NoError(err)
// proposal, found := suite.keeper.GetProposal(suite.ctx, res.ProposalID)
// suite.Require().True(found)
// msgVote := types.NewMsgVote(suite.addresses[0], proposal.ID, types.VOTE_TYPE_YES)
// _, err = suite.msgServer.Vote(sdk.WrapSDKContext(suite.ctx), msgVote)
// suite.Require().NoError(err)
// }
func TestMsgServerTestSuite(t *testing.T) {
suite.Run(t, new(MsgServerTestSuite))
}

View File

@ -0,0 +1,308 @@
package keeper
import (
"fmt"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/0glabs/0g-chain/x/committee/types"
)
// SubmitProposal adds a proposal to a committee so that it can be voted on.
func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, error) {
// Limit proposals to only be submitted by committee members
com, found := k.GetCommittee(ctx, committeeID)
if !found {
return 0, errorsmod.Wrapf(types.ErrUnknownCommittee, "%d", committeeID)
}
if !com.HasMember(proposer) {
return 0, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "proposer not member of committee")
}
// Check committee has permissions to enact proposal.
if !com.HasPermissionsFor(ctx, k.cdc, k.paramKeeper, pubProposal) {
return 0, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
}
// Check proposal is valid
if err := k.ValidatePubProposal(ctx, pubProposal); err != nil {
return 0, err
}
// Get a new ID and store the proposal
deadline := ctx.BlockTime().Add(com.GetProposalDuration())
proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline)
if err != nil {
return 0, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalSubmit,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.GetID())),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)),
sdk.NewAttribute(types.AttributeKeyDeadline, deadline.String()),
),
)
return proposalID, nil
}
// AddVote submits a vote on a proposal.
func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress, voteType types.VoteType) error {
// Validate
pr, found := k.GetProposal(ctx, proposalID)
if !found {
return errorsmod.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
}
if pr.HasExpiredBy(ctx.BlockTime()) {
return errorsmod.Wrapf(types.ErrProposalExpired, "%s ≥ %s", ctx.BlockTime(), pr.Deadline)
}
com, found := k.GetCommittee(ctx, pr.CommitteeID)
if !found {
return errorsmod.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID)
}
if _, ok := com.(*types.MemberCommittee); ok {
if !com.HasMember(voter) {
return errorsmod.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee")
}
if voteType != types.VOTE_TYPE_YES {
return errorsmod.Wrap(types.ErrInvalidVoteType, "member committees only accept yes votes")
}
}
// Store vote, overwriting any prior vote
k.SetVote(ctx, types.NewVote(proposalID, voter, voteType))
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalVote,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.GetID())),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", pr.ID)),
sdk.NewAttribute(types.AttributeKeyVoter, voter.String()),
sdk.NewAttribute(types.AttributeKeyVote, fmt.Sprintf("%d", voteType)),
),
)
return nil
}
// ValidatePubProposal checks if a pubproposal is valid.
func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr error) {
if pubProposal == nil {
return errorsmod.Wrap(types.ErrInvalidPubProposal, "pub proposal cannot be nil")
}
if err := pubProposal.ValidateBasic(); err != nil {
return err
}
if !k.router.HasRoute(pubProposal.ProposalRoute()) {
return errorsmod.Wrapf(types.ErrNoProposalHandlerExists, "%T", pubProposal)
}
// Run the proposal's changes through the associated handler using a cached version of state to ensure changes are not permanent.
cacheCtx, _ := ctx.CacheContext()
handler := k.router.GetRoute(pubProposal.ProposalRoute())
// Handle an edge case where a param change proposal causes the proposal handler to panic.
// A param change proposal with a registered subspace value but unregistered key value will cause a panic in the param change proposal handler.
// This defer will catch panics and return a normal error: `recover()` gets the panic value, then the enclosing function's return value is swapped for an error.
// reference: https://stackoverflow.com/questions/33167282/how-to-return-a-value-in-a-go-function-that-panics?noredirect=1&lq=1
defer func() {
if r := recover(); r != nil {
returnErr = errorsmod.Wrapf(types.ErrInvalidPubProposal, "proposal handler panicked: %s", r)
}
}()
if err := handler(cacheCtx, pubProposal); err != nil {
return err
}
return nil
}
func (k Keeper) ProcessProposals(ctx sdk.Context) {
k.IterateProposals(ctx, func(proposal types.Proposal) bool {
committee, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
k.CloseProposal(ctx, proposal, types.Failed)
return false
}
if !proposal.HasExpiredBy(ctx.BlockTime()) {
if committee.GetTallyOption() == types.TALLY_OPTION_FIRST_PAST_THE_POST {
passed := k.GetProposalResult(ctx, proposal.ID, committee)
if passed {
outcome := k.attemptEnactProposal(ctx, proposal)
k.CloseProposal(ctx, proposal, outcome)
}
}
} else {
passed := k.GetProposalResult(ctx, proposal.ID, committee)
outcome := types.Failed
if passed {
outcome = k.attemptEnactProposal(ctx, proposal)
}
k.CloseProposal(ctx, proposal, outcome)
}
return false
})
}
func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64, committee types.Committee) bool {
switch com := committee.(type) {
case *types.MemberCommittee:
return k.GetMemberCommitteeProposalResult(ctx, proposalID, com)
case *types.TokenCommittee:
return k.GetTokenCommitteeProposalResult(ctx, proposalID, com)
default: // Should never hit default case
return false
}
}
// GetMemberCommitteeProposalResult gets the result of a member committee proposal
func (k Keeper) GetMemberCommitteeProposalResult(ctx sdk.Context, proposalID uint64, committee types.Committee) bool {
currVotes := k.TallyMemberCommitteeVotes(ctx, proposalID)
possibleVotes := sdk.NewDec(int64(len(committee.GetMembers())))
return currVotes.GTE(committee.GetVoteThreshold().Mul(possibleVotes)) // vote threshold requirements
}
// TallyMemberCommitteeVotes returns the polling status of a member committee vote
func (k Keeper) TallyMemberCommitteeVotes(ctx sdk.Context, proposalID uint64) (totalVotes sdk.Dec) {
votes := k.GetVotesByProposal(ctx, proposalID)
return sdk.NewDec(int64(len(votes)))
}
// GetTokenCommitteeProposalResult gets the result of a token committee proposal
func (k Keeper) GetTokenCommitteeProposalResult(ctx sdk.Context, proposalID uint64, committee *types.TokenCommittee) bool {
yesVotes, noVotes, totalVotes, possibleVotes := k.TallyTokenCommitteeVotes(ctx, proposalID, committee.TallyDenom)
if totalVotes.GTE(committee.Quorum.Mul(possibleVotes)) { // quorum requirement
nonAbstainVotes := yesVotes.Add(noVotes)
if yesVotes.GTE(nonAbstainVotes.Mul(committee.VoteThreshold)) { // vote threshold requirements
return true
}
}
return false
}
// TallyMemberCommitteeVotes returns the polling status of a token committee vote. Returns yes votes,
// total current votes, total possible votes (equal to token supply), vote threshold (yes vote ratio
// required for proposal to pass), and quorum (votes tallied at this percentage).
func (k Keeper) TallyTokenCommitteeVotes(ctx sdk.Context, proposalID uint64,
tallyDenom string,
) (yesVotes, noVotes, totalVotes, possibleVotes sdk.Dec) {
votes := k.GetVotesByProposal(ctx, proposalID)
yesVotes = sdk.ZeroDec()
noVotes = sdk.ZeroDec()
totalVotes = sdk.ZeroDec()
for _, vote := range votes {
// 1 token = 1 vote
acc := k.accountKeeper.GetAccount(ctx, vote.Voter)
accNumCoins := k.bankKeeper.GetBalance(ctx, acc.GetAddress(), tallyDenom).Amount
// Add votes to counters
totalVotes = totalVotes.Add(sdk.NewDecFromInt(accNumCoins))
if vote.VoteType == types.VOTE_TYPE_YES {
yesVotes = yesVotes.Add(sdk.NewDecFromInt(accNumCoins))
} else if vote.VoteType == types.VOTE_TYPE_NO {
noVotes = noVotes.Add(sdk.NewDecFromInt(accNumCoins))
}
}
possibleVotesInt := k.bankKeeper.GetSupply(ctx, tallyDenom).Amount
return yesVotes, noVotes, totalVotes, sdk.NewDecFromInt(possibleVotesInt)
}
func (k Keeper) attemptEnactProposal(ctx sdk.Context, proposal types.Proposal) types.ProposalOutcome {
err := k.enactProposal(ctx, proposal)
if err != nil {
return types.Invalid
}
return types.Passed
}
// enactProposal makes the changes proposed in a proposal.
func (k Keeper) enactProposal(ctx sdk.Context, proposal types.Proposal) error {
// Check committee still has permissions for the proposal
// Since the proposal was submitted params could have changed, invalidating the permission of the committee.
com, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
return errorsmod.Wrapf(types.ErrUnknownCommittee, "%d", proposal.CommitteeID)
}
if !com.HasPermissionsFor(ctx, k.cdc, k.paramKeeper, proposal.GetContent()) {
return errorsmod.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
}
if err := k.ValidatePubProposal(ctx, proposal.GetContent()); err != nil {
return err
}
// enact the proposal
handler := k.router.GetRoute(proposal.GetContent().ProposalRoute())
if err := handler(ctx, proposal.GetContent()); err != nil {
// the handler should not error as it was checked in ValidatePubProposal
panic(fmt.Sprintf("unexpected handler error: %s", err))
}
return nil
}
// GetProposalTallyResponse returns the tally results of a proposal.
func (k Keeper) GetProposalTallyResponse(ctx sdk.Context, proposalID uint64) (*types.QueryTallyResponse, bool) {
proposal, found := k.GetProposal(ctx, proposalID)
if !found {
return nil, found
}
committee, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
return nil, found
}
var proposalTally types.QueryTallyResponse
switch com := committee.(type) {
case *types.MemberCommittee:
currVotes := k.TallyMemberCommitteeVotes(ctx, proposal.ID)
possibleVotes := sdk.NewDec(int64(len(com.Members)))
proposalTally = types.QueryTallyResponse{
ProposalID: proposal.ID,
YesVotes: currVotes,
NoVotes: sdk.ZeroDec(),
CurrentVotes: currVotes,
PossibleVotes: possibleVotes,
VoteThreshold: com.VoteThreshold,
Quorum: sdk.ZeroDec(),
}
case *types.TokenCommittee:
yesVotes, noVotes, currVotes, possibleVotes := k.TallyTokenCommitteeVotes(ctx, proposal.ID, com.TallyDenom)
proposalTally = types.QueryTallyResponse{
ProposalID: proposal.ID,
YesVotes: yesVotes,
NoVotes: noVotes,
CurrentVotes: currVotes,
PossibleVotes: possibleVotes,
VoteThreshold: com.VoteThreshold,
Quorum: com.Quorum,
}
}
return &proposalTally, true
}
// CloseProposal deletes proposals and their votes, emitting an event denoting the final status of the proposal
func (k Keeper) CloseProposal(ctx sdk.Context, proposal types.Proposal, outcome types.ProposalOutcome) {
tally, _ := k.GetProposalTallyResponse(ctx, proposal.ID)
k.DeleteProposalAndVotes(ctx, proposal.ID)
bz, err := k.cdc.MarshalJSON(tally)
if err != nil {
fmt.Println("error marshaling proposal tally to bytes:", tally.String())
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalClose,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)),
sdk.NewAttribute(types.AttributeKeyProposalTally, string(bz)),
sdk.NewAttribute(types.AttributeKeyProposalOutcome, outcome.String()),
),
)
}

File diff suppressed because it is too large Load Diff

146
x/committee/module.go Normal file
View File

@ -0,0 +1,146 @@
package committee
import (
"context"
"encoding/json"
"fmt"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/spf13/cobra"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/0glabs/0g-chain/x/committee/client/cli"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/types"
)
// ConsensusVersion defines the current module consensus version.
const ConsensusVersion = 1
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// ----------------------------------------------------------------------------
// AppModuleBasic
// ----------------------------------------------------------------------------
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// Registers legacy amino codec
func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
types.RegisterLegacyAminoCodec(cdc)
}
// RegisterInterfaces registers the module's interface types
func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) {
types.RegisterInterfaces(reg)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
return cdc.MustMarshalJSON(types.DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error {
var genState types.GenesisState
if err := cdc.UnmarshalJSON(bz, &genState); err != nil {
return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err)
}
return genState.Validate()
}
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for committee module.
func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {
types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx))
}
// GetTxCmd returns committee module's root tx command.
func (a AppModuleBasic) GetTxCmd() *cobra.Command {
return cli.GetTxCmd()
}
// GetQueryCmd returns committee module's root query command.
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
return cli.GetQueryCmd()
}
// ----------------------------------------------------------------------------
// AppModule
// ----------------------------------------------------------------------------
// AppModule implements the AppModule interface for committee module.
type AppModule struct {
AppModuleBasic
keeper keeper.Keeper
accountKeeper types.AccountKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper keeper.Keeper, accountKeeper types.AccountKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
accountKeeper: accountKeeper,
}
}
// Name returns committee module's name.
func (am AppModule) Name() string {
return am.AppModuleBasic.Name()
}
// RegisterServices registers a GRPC query service to respond to the
// module-specific GRPC queries.
func (am AppModule) RegisterServices(cfg module.Configurator) {
types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
types.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServerImpl(am.keeper))
}
// RegisterInvariants registers committee module's invariants.
func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// InitGenesis performs committee module's genesis initialization It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.RawMessage) []abci.ValidatorUpdate {
var genState types.GenesisState
cdc.MustUnmarshalJSON(gs, &genState)
InitGenesis(ctx, am.keeper, &genState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis returns committee module's exported genesis state as raw JSON bytes.
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
genState := ExportGenesis(ctx, am.keeper)
return cdc.MustMarshalJSON(genState)
}
// ConsensusVersion implements ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion }
// BeginBlock executes all ABCI BeginBlock logic respective to committee module.
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
BeginBlocker(ctx, req, am.keeper)
}
// EndBlock executes all ABCI EndBlock logic respective to committee module. It
// returns no validator updates.
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

View File

@ -0,0 +1,55 @@
package committee
import (
errorsmod "cosmossdk.io/errors"
"github.com/0glabs/0g-chain/x/committee/keeper"
"github.com/0glabs/0g-chain/x/committee/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
)
func NewProposalHandler(k keeper.Keeper) govv1beta1.Handler {
return func(ctx sdk.Context, content govv1beta1.Content) error {
switch c := content.(type) {
case *types.CommitteeChangeProposal:
return handleCommitteeChangeProposal(ctx, k, c)
case *types.CommitteeDeleteProposal:
return handleCommitteeDeleteProposal(ctx, k, c)
default:
return errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s proposal content type: %T", types.ModuleName, c)
}
}
}
func handleCommitteeChangeProposal(ctx sdk.Context, k keeper.Keeper, committeeProposal *types.CommitteeChangeProposal) error {
if err := committeeProposal.ValidateBasic(); err != nil {
return errorsmod.Wrap(types.ErrInvalidPubProposal, err.Error())
}
// Remove all committee's ongoing proposals
proposals := k.GetProposalsByCommittee(ctx, committeeProposal.GetNewCommittee().GetID())
for _, p := range proposals {
k.CloseProposal(ctx, p, types.Failed)
}
// update/create the committee
k.SetCommittee(ctx, committeeProposal.GetNewCommittee())
return nil
}
func handleCommitteeDeleteProposal(ctx sdk.Context, k keeper.Keeper, committeeProposal *types.CommitteeDeleteProposal) error {
if err := committeeProposal.ValidateBasic(); err != nil {
return errorsmod.Wrap(types.ErrInvalidPubProposal, err.Error())
}
// Remove all committee's ongoing proposals
proposals := k.GetProposalsByCommittee(ctx, committeeProposal.CommitteeID)
for _, p := range proposals {
k.CloseProposal(ctx, p, types.Failed)
}
k.DeleteCommittee(ctx, committeeProposal.CommitteeID)
return nil
}

Some files were not shown because too many files have changed in this diff Show More