597 lines
17 KiB
Go
597 lines
17 KiB
Go
package chain
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tcs-iptv/tcs/internal/model"
|
|
)
|
|
|
|
// MemoryChain 是 Client 的内存实现(MVP / 测试用)。
|
|
// 严格执行合约级业务规则:签发权限、1:1 不可解绑、映射前置签发、防重复哈希。
|
|
type MemoryChain struct {
|
|
mu sync.RWMutex
|
|
contents map[string]model.Content // ccCode -> Content
|
|
bindings map[string][]model.HashBinding // ccCode -> bindings
|
|
mappings map[string][]model.Mapping // ccCode -> mappings
|
|
versions map[string][]model.VersionChange
|
|
hashIndex map[string]string // fileHash -> ccCode(防换壳重发)
|
|
txSeq int
|
|
}
|
|
|
|
// NewMemoryChain 创建内存链客户端。
|
|
func NewMemoryChain() *MemoryChain {
|
|
return &MemoryChain{
|
|
contents: make(map[string]model.Content),
|
|
bindings: make(map[string][]model.HashBinding),
|
|
mappings: make(map[string][]model.Mapping),
|
|
versions: make(map[string][]model.VersionChange),
|
|
hashIndex: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (m *MemoryChain) nextTx(method string) string {
|
|
m.txSeq++
|
|
return fmt.Sprintf("tx-%s-%06d", method, m.txSeq)
|
|
}
|
|
|
|
// IssueMA 仅监管主体可调用;MA 不可重复签发;哈希 1:1 强绑定不可解绑。
|
|
func (m *MemoryChain) IssueMA(role Role, req IssueRequest) (string, error) {
|
|
if role != RoleRegulator {
|
|
return "", ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if _, ok := m.contents[req.CCCode]; ok {
|
|
return "", ErrMAAlreadyIssued
|
|
}
|
|
if existing, ok := m.hashIndex[req.FileHash]; ok {
|
|
return "", fmt.Errorf("%w: bound to %s", ErrHashExists, existing)
|
|
}
|
|
|
|
c := req.Content
|
|
c.CCCode = req.CCCode
|
|
c.ContentTwinID = req.ContentTwinID
|
|
c.Status = model.StatusApproved
|
|
if c.CreatedAt.IsZero() {
|
|
c.CreatedAt = time.Now()
|
|
}
|
|
m.contents[req.CCCode] = c
|
|
|
|
m.bindings[req.CCCode] = []model.HashBinding{{
|
|
ContentTwinID: req.ContentTwinID,
|
|
HashType: model.HashFile,
|
|
HashValue: req.FileHash,
|
|
MerkleRoot: req.MerkleRoot,
|
|
Version: "v1.0",
|
|
CreatedBy: string(RoleRegulator),
|
|
}}
|
|
if req.PerceptualHash != "" {
|
|
m.bindings[req.CCCode] = append(m.bindings[req.CCCode], model.HashBinding{
|
|
ContentTwinID: req.ContentTwinID,
|
|
HashType: model.HashPerceptual,
|
|
HashValue: req.PerceptualHash,
|
|
Version: "v1.0",
|
|
CreatedBy: string(RoleRegulator),
|
|
})
|
|
}
|
|
m.hashIndex[req.FileHash] = req.CCCode
|
|
|
|
// 集级哈希绑定(分集内容):每集独立哈希,挂在同一 MA 码下。
|
|
for _, ep := range req.Episodes {
|
|
m.bindings[req.CCCode] = append(m.bindings[req.CCCode], model.HashBinding{
|
|
ContentTwinID: req.ContentTwinID,
|
|
HashType: model.HashFile,
|
|
HashValue: ep.FileSHA256,
|
|
MerkleRoot: ep.MerkleRoot,
|
|
Episode: ep.Episode,
|
|
Resolution: ep.Resolution,
|
|
Duration: ep.Duration,
|
|
Version: "v1.0",
|
|
CreatedBy: string(RoleRegulator),
|
|
})
|
|
if ep.FileSHA256 != "" {
|
|
if _, ok := m.hashIndex[ep.FileSHA256]; !ok {
|
|
m.hashIndex[ep.FileSHA256] = req.CCCode
|
|
}
|
|
}
|
|
}
|
|
return m.nextTx("issueMA"), nil
|
|
}
|
|
|
|
// RegisterHashBinding 追加哈希绑定(如转码版)。MA 必须已签发。
|
|
func (m *MemoryChain) RegisterHashBinding(role Role, b model.HashBinding) (string, error) {
|
|
if role != RoleReviewer && role != RoleRegulator {
|
|
return "", ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
ccCode := m.ccCodeByCTID(b.ContentTwinID)
|
|
if ccCode == "" {
|
|
return "", ErrMANotIssued
|
|
}
|
|
m.bindings[ccCode] = append(m.bindings[ccCode], b)
|
|
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
|
|
if _, ok := m.hashIndex[b.HashValue]; !ok {
|
|
m.hashIndex[b.HashValue] = ccCode
|
|
}
|
|
}
|
|
return m.nextTx("registerHashBinding"), nil
|
|
}
|
|
|
|
// RegisterMapping 注册三方编码映射;MA 必须已签发(需求16-AC3)。
|
|
func (m *MemoryChain) RegisterMapping(role Role, mp model.Mapping) (string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
ccCode := m.ccCodeByCTID(mp.ContentTwinID)
|
|
if ccCode == "" {
|
|
return "", ErrMANotIssued
|
|
}
|
|
m.mappings[ccCode] = append(m.mappings[ccCode], mp)
|
|
return m.nextTx("registerMapping"), nil
|
|
}
|
|
|
|
// VerifyHash 按 MA 码校验提交哈希。
|
|
func (m *MemoryChain) VerifyHash(ccCode, fileHash string) (VerifyResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
bs, ok := m.bindings[ccCode]
|
|
if !ok {
|
|
return VerifyResult{Valid: false, CCCode: ccCode, SubmittedHash: fileHash}, ErrMANotIssued
|
|
}
|
|
for _, b := range bs {
|
|
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
|
|
if b.HashValue == fileHash {
|
|
return VerifyResult{
|
|
Valid: true, CCCode: ccCode,
|
|
BoundHash: b.HashValue, SubmittedHash: fileHash,
|
|
Match: true, Version: b.Version,
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
// 取首个文件哈希作为 bound 参考
|
|
bound := ""
|
|
for _, b := range bs {
|
|
if b.HashType == model.HashFile {
|
|
bound = b.HashValue
|
|
break
|
|
}
|
|
}
|
|
return VerifyResult{Valid: true, CCCode: ccCode, BoundHash: bound, SubmittedHash: fileHash, Match: false}, nil
|
|
}
|
|
|
|
// HashExists 判断内容哈希是否已存在。
|
|
func (m *MemoryChain) HashExists(fileHash string) (string, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
ma, ok := m.hashIndex[fileHash]
|
|
return ma, ok
|
|
}
|
|
|
|
// VerifyEpisodeHash 按 MA 码+集号校验该集哈希。
|
|
func (m *MemoryChain) VerifyEpisodeHash(ccCode string, episode int, fileHash string) (VerifyResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
bs, ok := m.bindings[ccCode]
|
|
if !ok {
|
|
return VerifyResult{Valid: false, CCCode: ccCode, SubmittedHash: fileHash}, ErrMANotIssued
|
|
}
|
|
var bound string
|
|
for _, b := range bs {
|
|
if b.Episode == episode && (b.HashType == model.HashFile || b.HashType == model.HashTranscoded) {
|
|
if bound == "" {
|
|
bound = b.HashValue
|
|
}
|
|
if b.HashValue == fileHash {
|
|
return VerifyResult{
|
|
Valid: true, CCCode: ccCode,
|
|
BoundHash: b.HashValue, SubmittedHash: fileHash,
|
|
Match: true, Version: b.Version,
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
if bound == "" {
|
|
return VerifyResult{Valid: false, CCCode: ccCode, SubmittedHash: fileHash}, ErrNotFound
|
|
}
|
|
return VerifyResult{Valid: true, CCCode: ccCode, BoundHash: bound, SubmittedHash: fileHash, Match: false}, nil
|
|
}
|
|
|
|
// ListEpisodes 返回某 MA 码下的全部集级哈希绑定(episode > 0)。
|
|
func (m *MemoryChain) ListEpisodes(ccCode string) ([]model.HashBinding, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
bs, ok := m.bindings[ccCode]
|
|
if !ok {
|
|
return nil, ErrMANotIssued
|
|
}
|
|
var out []model.HashBinding
|
|
for _, b := range bs {
|
|
if b.Episode > 0 {
|
|
out = append(out, b)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// QueryContent 查询内容主记录。
|
|
func (m *MemoryChain) QueryContent(ccCode string) (model.Content, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
c, ok := m.contents[ccCode]
|
|
if !ok {
|
|
return model.Content{}, ErrNotFound
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// ListContents 按状态列出内容(空状态返回全部),附带整剧文件哈希便于演示。
|
|
func (m *MemoryChain) ListContents(status string) ([]model.Content, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
var out []model.Content
|
|
for ma, c := range m.contents {
|
|
if status == "" || c.Status == status {
|
|
// 附带整剧文件哈希(episode==0 的 file 绑定)
|
|
for _, b := range m.bindings[ma] {
|
|
if b.HashType == model.HashFile && b.Episode == 0 {
|
|
c.FileHash = b.HashValue
|
|
break
|
|
}
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// QueryMappings 查询 MA 码绑定的全部映射与 CDN 端点。
|
|
func (m *MemoryChain) QueryMappings(ccCode string) (MappingsResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if _, ok := m.contents[ccCode]; !ok {
|
|
return MappingsResult{}, ErrNotFound
|
|
}
|
|
res := MappingsResult{CCCode: ccCode, Mappings: m.mappings[ccCode]}
|
|
for _, mp := range m.mappings[ccCode] {
|
|
if mp.CDNEndpoint != "" {
|
|
res.CDNEndpoints = append(res.CDNEndpoints, mp.CDNEndpoint)
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// RecordVersionChange 记录版本变更。
|
|
func (m *MemoryChain) RecordVersionChange(vc model.VersionChange) (string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ccCode := m.ccCodeByCTID(vc.ContentTwinID)
|
|
if ccCode == "" {
|
|
return "", ErrMANotIssued
|
|
}
|
|
m.versions[ccCode] = append(m.versions[ccCode], vc)
|
|
return m.nextTx("recordVersionChange"), nil
|
|
}
|
|
|
|
// Revoke 下架,仅监管主体。
|
|
func (m *MemoryChain) Revoke(role Role, ccCode, reason string) (MappingsResult, error) {
|
|
if role != RoleRegulator {
|
|
return MappingsResult{}, ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
c, ok := m.contents[ccCode]
|
|
if !ok {
|
|
return MappingsResult{}, ErrNotFound
|
|
}
|
|
c.Status = model.StatusRevoked
|
|
m.contents[ccCode] = c
|
|
|
|
res := MappingsResult{CCCode: ccCode, Mappings: m.mappings[ccCode]}
|
|
for _, mp := range m.mappings[ccCode] {
|
|
if mp.CDNEndpoint != "" {
|
|
res.CDNEndpoints = append(res.CDNEndpoints, mp.CDNEndpoint)
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// RevokeEpisode 集级下架:只下架指定集,整剧其他集不受影响(仅监管主体)。
|
|
func (m *MemoryChain) RevokeEpisode(role Role, ccCode string, episode int, reason string) error {
|
|
if role != RoleRegulator {
|
|
return ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
bs, ok := m.bindings[ccCode]
|
|
if !ok {
|
|
return ErrMANotIssued
|
|
}
|
|
found := false
|
|
for i := range bs {
|
|
if bs[i].Episode == episode {
|
|
bs[i].Revoked = true
|
|
bs[i].RevokedReason = reason
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return ErrNotFound
|
|
}
|
|
m.bindings[ccCode] = bs
|
|
return nil
|
|
}
|
|
|
|
// Restore 恢复上架整剧:下架状态恢复为流通中(仅监管主体)。
|
|
func (m *MemoryChain) Restore(role Role, ccCode string) error {
|
|
if role != RoleRegulator {
|
|
return ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
c, ok := m.contents[ccCode]
|
|
if !ok {
|
|
return ErrNotFound
|
|
}
|
|
c.Status = model.StatusPublished
|
|
m.contents[ccCode] = c
|
|
return nil
|
|
}
|
|
|
|
// RestoreEpisode 恢复上架指定集(仅监管主体)。
|
|
func (m *MemoryChain) RestoreEpisode(role Role, ccCode string, episode int) error {
|
|
if role != RoleRegulator {
|
|
return ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
bs, ok := m.bindings[ccCode]
|
|
if !ok {
|
|
return ErrMANotIssued
|
|
}
|
|
found := false
|
|
for i := range bs {
|
|
if bs[i].Episode == episode {
|
|
bs[i].Revoked = false
|
|
bs[i].RevokedReason = ""
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return ErrNotFound
|
|
}
|
|
m.bindings[ccCode] = bs
|
|
return nil
|
|
}
|
|
|
|
// SetContentStatus 更新内容状态。
|
|
func (m *MemoryChain) SetContentStatus(ccCode, status string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
c, ok := m.contents[ccCode]
|
|
if !ok {
|
|
return ErrNotFound
|
|
}
|
|
c.Status = status
|
|
m.contents[ccCode] = c
|
|
return nil
|
|
}
|
|
|
|
// ccCodeByCTID 内部辅助:通过 CTID 反查 MA 码(调用方已持锁)。
|
|
func (m *MemoryChain) ccCodeByCTID(ctid string) string {
|
|
for ma, c := range m.contents {
|
|
if c.ContentTwinID == ctid {
|
|
return ma
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// QueryByHash 根据内容哈希反查标识信息及映射关系。
|
|
func (m *MemoryChain) QueryByHash(fileHash string) (model.ContentQueryResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
ccCode, ok := m.hashIndex[fileHash]
|
|
if !ok {
|
|
return model.ContentQueryResult{Found: false}, ErrNotFound
|
|
}
|
|
return model.ContentQueryResult{
|
|
Found: true,
|
|
Content: m.contents[ccCode],
|
|
Bindings: m.bindings[ccCode],
|
|
Mappings: m.mappings[ccCode],
|
|
}, nil
|
|
}
|
|
|
|
// QueryByProvincialCode 根据省级内容编码(CP MediaID)反查标识信息。
|
|
func (m *MemoryChain) QueryByProvincialCode(provincialCode string) (model.ContentQueryResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
for ccCode, maps := range m.mappings {
|
|
for _, mp := range maps {
|
|
if mp.Party == model.PartyCP && mp.PartyID == provincialCode {
|
|
return model.ContentQueryResult{
|
|
Found: true,
|
|
Content: m.contents[ccCode],
|
|
Bindings: m.bindings[ccCode],
|
|
Mappings: maps,
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
return model.ContentQueryResult{Found: false}, ErrNotFound
|
|
}
|
|
|
|
// QueryByLibraryFileID 根据片库文件 ID(媒资库 ID)反查标识信息。
|
|
func (m *MemoryChain) QueryByLibraryFileID(libraryFileID string) (model.ContentQueryResult, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
for ccCode, maps := range m.mappings {
|
|
for _, mp := range maps {
|
|
if mp.Party == model.PartyReviewer && mp.PartyID == libraryFileID {
|
|
return model.ContentQueryResult{
|
|
Found: true,
|
|
Content: m.contents[ccCode],
|
|
Bindings: m.bindings[ccCode],
|
|
Mappings: maps,
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
return model.ContentQueryResult{Found: false}, ErrNotFound
|
|
}
|
|
|
|
// MergeMA 将多个 MA 码合并为一个主 MA 码(仅监管主体)。
|
|
// 被合并的 MA 码的哈希绑定和映射迁移至主 MA 码,原 MA 码状态标记为 merged。
|
|
func (m *MemoryChain) MergeMA(role Role, req model.MergeRequest) (model.MergeResult, error) {
|
|
if role != RoleRegulator {
|
|
return model.MergeResult{}, ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
primary, ok := m.contents[req.PrimaryCCCode]
|
|
if !ok {
|
|
return model.MergeResult{}, ErrNotFound
|
|
}
|
|
_ = primary
|
|
|
|
migratedBindings := 0
|
|
migratedMappings := 0
|
|
for _, secMA := range req.SecondaryCCCodes {
|
|
secContent, exists := m.contents[secMA]
|
|
if !exists {
|
|
return model.MergeResult{}, fmt.Errorf("%w: secondary MA %s not found", ErrNotFound, secMA)
|
|
}
|
|
if secContent.Status == model.StatusMerged {
|
|
return model.MergeResult{}, fmt.Errorf("chain: MA %s already merged", secMA)
|
|
}
|
|
|
|
// 迁移哈希绑定至主 MA 码
|
|
for _, b := range m.bindings[secMA] {
|
|
b.ContentTwinID = primary.ContentTwinID
|
|
m.bindings[req.PrimaryCCCode] = append(m.bindings[req.PrimaryCCCode], b)
|
|
migratedBindings++
|
|
}
|
|
|
|
// 迁移映射至主 MA 码
|
|
for _, mp := range m.mappings[secMA] {
|
|
mp.ContentTwinID = primary.ContentTwinID
|
|
m.mappings[req.PrimaryCCCode] = append(m.mappings[req.PrimaryCCCode], mp)
|
|
migratedMappings++
|
|
}
|
|
|
|
// 更新哈希索引指向主 MA 码
|
|
for _, b := range m.bindings[secMA] {
|
|
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
|
|
m.hashIndex[b.HashValue] = req.PrimaryCCCode
|
|
}
|
|
}
|
|
|
|
// 清空被合并 MA 码的绑定和映射,状态标记为 merged
|
|
m.bindings[secMA] = nil
|
|
m.mappings[secMA] = nil
|
|
secContent.Status = model.StatusMerged
|
|
m.contents[secMA] = secContent
|
|
}
|
|
|
|
txID := m.nextTx("mergeMA")
|
|
return model.MergeResult{
|
|
PrimaryCCCode: req.PrimaryCCCode,
|
|
MergedCCCodes: req.SecondaryCCCodes,
|
|
MigratedBindings: migratedBindings,
|
|
MigratedMappings: migratedMappings,
|
|
TxID: txID,
|
|
}, nil
|
|
}
|
|
|
|
// SplitMA 将一个 MA 码拆分为多个独立 MA 码(仅监管主体)。
|
|
// 按集号将哈希绑定和映射迁移至新 MA 码,源 MA 码状态标记为 split。
|
|
func (m *MemoryChain) SplitMA(role Role, req model.SplitRequest) (model.SplitResult, error) {
|
|
if role != RoleRegulator {
|
|
return model.SplitResult{}, ErrPermissionDenied
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
srcContent, ok := m.contents[req.SourceCCCode]
|
|
if !ok {
|
|
return model.SplitResult{}, ErrNotFound
|
|
}
|
|
if srcContent.Status == model.StatusMerged || srcContent.Status == model.StatusSplit {
|
|
return model.SplitResult{}, fmt.Errorf("chain: MA %s already %s", req.SourceCCCode, srcContent.Status)
|
|
}
|
|
|
|
migratedBindings := 0
|
|
migratedMappings := 0
|
|
newCCCodes := make([]string, 0, len(req.Splits))
|
|
|
|
for _, split := range req.Splits {
|
|
// 创建新内容记录
|
|
newContent := srcContent
|
|
newContent.CCCode = split.NewCCCode
|
|
newContent.ContentTwinID = split.NewCCCode + "-ctid"
|
|
newContent.Title = split.Title
|
|
newContent.Status = model.StatusApproved
|
|
newContent.CreatedAt = time.Now()
|
|
m.contents[split.NewCCCode] = newContent
|
|
|
|
// 按集号迁移哈希绑定
|
|
for _, b := range m.bindings[req.SourceCCCode] {
|
|
shouldMigrate := false
|
|
if len(split.Episodes) == 0 {
|
|
// 空集号列表:迁移全部(整剧拆分场景)
|
|
shouldMigrate = true
|
|
} else {
|
|
for _, ep := range split.Episodes {
|
|
if b.Episode == ep {
|
|
shouldMigrate = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if shouldMigrate {
|
|
newB := b
|
|
newB.ContentTwinID = newContent.ContentTwinID
|
|
m.bindings[split.NewCCCode] = append(m.bindings[split.NewCCCode], newB)
|
|
// 更新哈希索引
|
|
if b.HashType == model.HashFile || b.HashType == model.HashTranscoded {
|
|
m.hashIndex[b.HashValue] = split.NewCCCode
|
|
}
|
|
migratedBindings++
|
|
}
|
|
}
|
|
|
|
// 迁移映射(全部映射复制到新 MA 码)
|
|
for _, mp := range m.mappings[req.SourceCCCode] {
|
|
newMP := mp
|
|
newMP.ContentTwinID = newContent.ContentTwinID
|
|
m.mappings[split.NewCCCode] = append(m.mappings[split.NewCCCode], newMP)
|
|
migratedMappings++
|
|
}
|
|
|
|
newCCCodes = append(newCCCodes, split.NewCCCode)
|
|
}
|
|
|
|
// 源 MA 码状态标记为 split
|
|
srcContent.Status = model.StatusSplit
|
|
m.contents[req.SourceCCCode] = srcContent
|
|
|
|
txID := m.nextTx("splitMA")
|
|
return model.SplitResult{
|
|
SourceCCCode: req.SourceCCCode,
|
|
NewCCCodes: newCCCodes,
|
|
MigratedBindings: migratedBindings,
|
|
MigratedMappings: migratedMappings,
|
|
TxID: txID,
|
|
}, nil
|
|
}
|
|
|
|
var _ Client = (*MemoryChain)(nil)
|