| @@ -5,6 +5,7 @@ import com.jd.blockchain.binaryproto.DataContract; | |||||
| import com.jd.blockchain.consts.DataCodes; | import com.jd.blockchain.consts.DataCodes; | ||||
| import com.jd.blockchain.ledger.*; | import com.jd.blockchain.ledger.*; | ||||
| import com.jd.blockchain.utils.Bytes; | import com.jd.blockchain.utils.Bytes; | ||||
| import com.jd.blockchain.utils.IllegalDataException; | |||||
| import org.springframework.util.ReflectionUtils; | import org.springframework.util.ReflectionUtils; | ||||
| import java.math.BigDecimal; | import java.math.BigDecimal; | ||||
| import java.nio.ByteBuffer; | import java.nio.ByteBuffer; | ||||
| @@ -117,7 +118,7 @@ public class ContractSerializeUtils { | |||||
| DATA_CONTRACT_MAP.put(DataCodes.CONTRACT_TEXT, CONTRACT_TEXT.class); | DATA_CONTRACT_MAP.put(DataCodes.CONTRACT_TEXT, CONTRACT_TEXT.class); | ||||
| DATA_CONTRACT_MAP.put(DataCodes.CONTRACT_BINARY, CONTRACT_BINARY.class); | DATA_CONTRACT_MAP.put(DataCodes.CONTRACT_BINARY, CONTRACT_BINARY.class); | ||||
| DATA_CONTRACT_MAP.put(DataCodes.CONTRACT_BIG_INT, CONTRACT_BIG_INT.class); | DATA_CONTRACT_MAP.put(DataCodes.CONTRACT_BIG_INT, CONTRACT_BIG_INT.class); | ||||
| // DATA_CONTRACT_MAP.put(DataCodes.TX_CONTENT_BODY, TransactionContentBody.class); | |||||
| DATA_CONTRACT_MAP.put(DataCodes.CONTRACT_BIZ_CONTENT, ContractBizContent.class); | |||||
| return DATA_CONTRACT_MAP; | return DATA_CONTRACT_MAP; | ||||
| } | } | ||||
| @@ -140,8 +141,12 @@ public class ContractSerializeUtils { | |||||
| return (CONTRACT_BINARY) () -> (Bytes) object; | return (CONTRACT_BINARY) () -> (Bytes) object; | ||||
| }else if(getDataIntf().get(dataContract.code()).equals(CONTRACT_BIG_INT.class)){ | }else if(getDataIntf().get(dataContract.code()).equals(CONTRACT_BIG_INT.class)){ | ||||
| return (CONTRACT_BIG_INT) () -> new BigDecimal(object.toString()); | return (CONTRACT_BIG_INT) () -> new BigDecimal(object.toString()); | ||||
| }else if(getDataIntf().get(dataContract.code()).equals(ContractBizContent.class)){ | |||||
| ContractBizContent contractBizContent = (ContractBizContent)object; | |||||
| return contractBizContent; | |||||
| }else { | |||||
| throw new IllegalDataException("cann't get new Object by dataContract and object."); | |||||
| } | } | ||||
| return null; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -164,6 +169,8 @@ public class ContractSerializeUtils { | |||||
| return CONTRACT_TEXT.class; | return CONTRACT_TEXT.class; | ||||
| }else if(classType.equals(Bytes.class)){ | }else if(classType.equals(Bytes.class)){ | ||||
| return CONTRACT_BINARY.class; | return CONTRACT_BINARY.class; | ||||
| }else if(classType.equals(BigDecimal.class)){ | |||||
| return CONTRACT_BIG_INT.class; | |||||
| } | } | ||||
| return null; | return null; | ||||
| } | } | ||||
| @@ -22,9 +22,16 @@ public interface ContractBizContent { | |||||
| HashDigest getLedgerHash(); | HashDigest getLedgerHash(); | ||||
| /** | /** | ||||
| * 操作列表; | |||||
| * 地址; | |||||
| * @return | * @return | ||||
| */ | */ | ||||
| @DataField(order = 2, list = true, refContract = true, genericContract = true) | |||||
| Operation[] getOperations(); | |||||
| @DataField(order = 2, primitiveType = PrimitiveType.TEXT) | |||||
| String getAddr(); | |||||
| /** | |||||
| * 年龄; | |||||
| * @return | |||||
| */ | |||||
| @DataField(order = 3, primitiveType = PrimitiveType.INT32) | |||||
| int getAge(); | |||||
| } | } | ||||
| @@ -62,7 +62,7 @@ public class GatewayServiceFactory implements BlockchainServiceFactory, Closeabl | |||||
| DataContractRegistry.register(CONTRACT_INT64.class); | DataContractRegistry.register(CONTRACT_INT64.class); | ||||
| DataContractRegistry.register(CONTRACT_TEXT.class); | DataContractRegistry.register(CONTRACT_TEXT.class); | ||||
| DataContractRegistry.register(CONTRACT_BINARY.class); | DataContractRegistry.register(CONTRACT_BINARY.class); | ||||
| // DataContractRegistry.register(CONTRACT_BIG_INT.class); | |||||
| DataContractRegistry.register(ContractBizContent.class); | |||||
| ByteArrayObjectUtil.init(); | ByteArrayObjectUtil.init(); | ||||
| } | } | ||||
| @@ -4,9 +4,12 @@ import com.jd.blockchain.binaryproto.DataContract; | |||||
| import com.jd.blockchain.consts.DataCodes; | import com.jd.blockchain.consts.DataCodes; | ||||
| import com.jd.blockchain.contract.Contract; | import com.jd.blockchain.contract.Contract; | ||||
| import com.jd.blockchain.contract.ContractEvent; | import com.jd.blockchain.contract.ContractEvent; | ||||
| import com.jd.blockchain.ledger.ContractBizContent; | |||||
| import com.jd.blockchain.ledger.TransactionContentBody; | import com.jd.blockchain.ledger.TransactionContentBody; | ||||
| import com.jd.blockchain.utils.Bytes; | import com.jd.blockchain.utils.Bytes; | ||||
| import java.math.BigDecimal; | |||||
| /** | /** | ||||
| * 示例:一个“资产管理”智能合约; | * 示例:一个“资产管理”智能合约; | ||||
| * | * | ||||
| @@ -22,19 +25,16 @@ public interface AssetContract2 { | |||||
| * 新发行的资产的持有账户; | * 新发行的资产的持有账户; | ||||
| */ | */ | ||||
| @ContractEvent(name = "issue-asset-0") | @ContractEvent(name = "issue-asset-0") | ||||
| void issue(@DataContract(code = DataCodes.TX_CONTENT_BODY) TransactionContentBody transactionContentBody, | |||||
| @DataContract(code = DataCodes.CONTRACT_TEXT) String assetHolderAddress); | |||||
| void issue(ContractBizContent contractBizContent, String assetHolderAddress); | |||||
| /** | /** | ||||
| * issue asset; | * issue asset; | ||||
| * @param transactionContentBody | |||||
| * @param contractBizContent | |||||
| * @param assetHolderAddress | * @param assetHolderAddress | ||||
| * @param cashNumber | * @param cashNumber | ||||
| */ | */ | ||||
| @ContractEvent(name = "issue-asset") | @ContractEvent(name = "issue-asset") | ||||
| public void issue(@DataContract(code = DataCodes.TX_CONTENT_BODY) TransactionContentBody transactionContentBody, | |||||
| @DataContract(code = DataCodes.CONTRACT_TEXT) String assetHolderAddress, | |||||
| @DataContract(code = DataCodes.CONTRACT_INT64) long cashNumber); | |||||
| public void issue(ContractBizContent contractBizContent, String assetHolderAddress, long cashNumber); | |||||
| /** | /** | ||||
| * Bytes can bring the byte[]; | * Bytes can bring the byte[]; | ||||
| @@ -49,5 +49,5 @@ public interface AssetContract2 { | |||||
| void issue(Byte bytes, String assetHolderAddress, long cashNumber); | void issue(Byte bytes, String assetHolderAddress, long cashNumber); | ||||
| @ContractEvent(name = "issue-asset-4") | @ContractEvent(name = "issue-asset-4") | ||||
| void issue1(byte[] bytes, String assetHolderAddress, long cashNumber); | |||||
| void issue(Byte byteObj, String assetHolderAddress, Bytes cashNumber); | |||||
| } | } | ||||
| @@ -23,7 +23,9 @@ import org.springframework.core.io.ClassPathResource; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.FileInputStream; | import java.io.FileInputStream; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.math.BigDecimal; | |||||
| import static org.junit.Assert.assertEquals; | |||||
| import static org.junit.Assert.assertTrue; | import static org.junit.Assert.assertTrue; | ||||
| /** | /** | ||||
| @@ -64,21 +66,27 @@ public class SDK_Contract_Test { | |||||
| /** | /** | ||||
| * 演示合约执行的过程; | * 演示合约执行的过程; | ||||
| */ | */ | ||||
| @Test | |||||
| // @Test | |||||
| public void demoContract1() { | public void demoContract1() { | ||||
| String dataAddress = registerData4Contract(); | |||||
| // 发起交易; | // 发起交易; | ||||
| TransactionTemplate txTemp = bcsrv.newTransaction(ledgerHash); | TransactionTemplate txTemp = bcsrv.newTransaction(ledgerHash); | ||||
| String contractAddress = "LdeNhjPGzHcHL6rLcJ7whHxUbn9Tv7qSKRfEA"; | |||||
| String contractAddress = "LdeNg8JHFCKABJt6AaRNVCZPgY4ofGPd8MgcR"; | |||||
| AssetContract2 assetContract = txTemp.contract(contractAddress, AssetContract2.class); | AssetContract2 assetContract = txTemp.contract(contractAddress, AssetContract2.class); | ||||
| TransactionContentBody transactionContentBody = new TransactionContentBody() { | |||||
| ContractBizContent contractBizContent = new ContractBizContent() { | |||||
| @Override | @Override | ||||
| public HashDigest getLedgerHash() { | public HashDigest getLedgerHash() { | ||||
| return new HashDigest(ClassicAlgorithm.SHA256, "zhaogw".getBytes()); | return new HashDigest(ClassicAlgorithm.SHA256, "zhaogw".getBytes()); | ||||
| } | } | ||||
| @Override | @Override | ||||
| public Operation[] getOperations() { | |||||
| return new Operation[0]; | |||||
| public String getAddr() { | |||||
| return "jinghailu street."; | |||||
| } | |||||
| @Override | |||||
| public int getAge() { | |||||
| return 100; | |||||
| } | } | ||||
| }; | }; | ||||
| // assetContract.issue(transactionContentBody,contractAddress); | // assetContract.issue(transactionContentBody,contractAddress); | ||||
| @@ -86,13 +94,19 @@ public class SDK_Contract_Test { | |||||
| // assetContract.issue(Bytes.fromString("zhaogw, contract based interface is OK!"),contractAddress,77777); | // assetContract.issue(Bytes.fromString("zhaogw, contract based interface is OK!"),contractAddress,77777); | ||||
| // assetContract.issue(Bytes.fromString("zhaogw, contract based interface is OK!"),contractAddress,77777); | // assetContract.issue(Bytes.fromString("zhaogw, contract based interface is OK!"),contractAddress,77777); | ||||
| Byte byteObj = Byte.parseByte("127"); | Byte byteObj = Byte.parseByte("127"); | ||||
| assetContract.issue(byteObj,contractAddress,321123); | |||||
| assetContract.issue(byteObj,dataAddress,321123); | |||||
| assetContract.issue(contractBizContent,dataAddress); | |||||
| assetContract.issue(Byte.parseByte("126"),dataAddress,Bytes.fromString("100.234")); | |||||
| // TX 准备就绪; | // TX 准备就绪; | ||||
| PreparedTransaction prepTx = txTemp.prepare(); | PreparedTransaction prepTx = txTemp.prepare(); | ||||
| prepTx.sign(signKeyPair); | prepTx.sign(signKeyPair); | ||||
| // 提交交易; | // 提交交易; | ||||
| prepTx.commit(); | |||||
| TransactionResponse transactionResponse = prepTx.commit(); | |||||
| //check; | |||||
| KVDataEntry[] dataEntries = bcsrv.getDataEntries(ledgerHash,dataAddress,"total"); | |||||
| assertEquals("100",dataEntries[0].getValue().toString()); | |||||
| } | } | ||||
| @Test | @Test | ||||
| @@ -126,6 +140,22 @@ public class SDK_Contract_Test { | |||||
| this.setDataInDataAddress(dataAccount.getAddress(),keys,values2,1); | this.setDataInDataAddress(dataAccount.getAddress(),keys,values2,1); | ||||
| } | } | ||||
| private String registerData4Contract(){ | |||||
| // 在本地定义 TX 模板 | |||||
| TransactionTemplate txTemp = bcsrv.newTransaction(ledgerHash); | |||||
| BlockchainKeypair dataAccount = BlockchainKeyGenerator.getInstance().generate(); | |||||
| txTemp.dataAccounts().register(dataAccount.getIdentity()); | |||||
| txTemp.dataAccount(dataAccount.getAddress()).set("total", 200, -1); | |||||
| // TX 准备就绪; | |||||
| PreparedTransaction prepTx = txTemp.prepare(); | |||||
| prepTx.sign(signKeyPair); | |||||
| // 提交交易; | |||||
| TransactionResponse transactionResponse = prepTx.commit(); | |||||
| assertTrue(transactionResponse.isSuccess()); | |||||
| return dataAccount.getAddress().toBase58(); | |||||
| } | |||||
| private void setDataInDataAddress(Bytes dataAddress, String[] keys, String[] values, long version){ | private void setDataInDataAddress(Bytes dataAddress, String[] keys, String[] values, long version){ | ||||
| // 在本地定义 TX 模板 | // 在本地定义 TX 模板 | ||||
| TransactionTemplate txTemp = bcsrv.newTransaction(ledgerHash); | TransactionTemplate txTemp = bcsrv.newTransaction(ledgerHash); | ||||
| @@ -35,6 +35,7 @@ import java.io.File; | |||||
| import java.io.FileInputStream; | import java.io.FileInputStream; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| import java.math.BigDecimal; | |||||
| import java.net.URL; | import java.net.URL; | ||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||
| import java.util.Arrays; | import java.util.Arrays; | ||||
| @@ -54,6 +55,7 @@ import static org.junit.Assert.*; | |||||
| */ | */ | ||||
| public class IntegrationBase { | public class IntegrationBase { | ||||
| public static String KEY_TOTAL = "total"; | |||||
| static { | static { | ||||
| DataContractRegistry.register(LedgerInitOperation.class); | DataContractRegistry.register(LedgerInitOperation.class); | ||||
| @@ -107,6 +109,7 @@ public class IntegrationBase { | |||||
| // 定义交易; | // 定义交易; | ||||
| TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); | TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); | ||||
| txTpl.dataAccounts().register(dataAccount.getIdentity()); | txTpl.dataAccounts().register(dataAccount.getIdentity()); | ||||
| txTpl.dataAccount(dataAccount.getAddress()).set("total", 200, -1); | |||||
| // 签名; | // 签名; | ||||
| PreparedTransaction ptx = txTpl.prepare(); | PreparedTransaction ptx = txTpl.prepare(); | ||||
| @@ -440,6 +443,8 @@ public class IntegrationBase { | |||||
| static HashDigest txContentHash; | static HashDigest txContentHash; | ||||
| public static LedgerBlock testSDK_Contract(AsymmetricKeypair adminKey, HashDigest ledgerHash, | public static LedgerBlock testSDK_Contract(AsymmetricKeypair adminKey, HashDigest ledgerHash, | ||||
| BlockchainService blockchainService,LedgerRepository ledgerRepository) { | BlockchainService blockchainService,LedgerRepository ledgerRepository) { | ||||
| KeyPairResponse keyPairResponse = testSDK_RegisterDataAccount(adminKey,ledgerHash,blockchainService); | |||||
| System.out.println("adminKey="+ AddressEncoding.generateAddress(adminKey.getPubKey())); | System.out.println("adminKey="+ AddressEncoding.generateAddress(adminKey.getPubKey())); | ||||
| BlockchainKeypair userKey = BlockchainKeyGenerator.getInstance().generate(); | BlockchainKeypair userKey = BlockchainKeyGenerator.getInstance().generate(); | ||||
| System.out.println("userKey="+userKey.getAddress()); | System.out.println("userKey="+userKey.getAddress()); | ||||
| @@ -458,32 +463,34 @@ public class IntegrationBase { | |||||
| TransactionResponse txResp = ptx.commit(); | TransactionResponse txResp = ptx.commit(); | ||||
| assertTrue(txResp.isSuccess()); | assertTrue(txResp.isSuccess()); | ||||
| // 验证结果; | |||||
| txResp.getContentHash(); | |||||
| // 验证结果hash;请求的hash=相应的内容hash; | |||||
| assertEquals(ptx.getHash(),txResp.getContentHash()); | |||||
| LedgerBlock block = ledgerRepository.getBlock(txResp.getBlockHeight()); | LedgerBlock block = ledgerRepository.getBlock(txResp.getBlockHeight()); | ||||
| byte[] contractCodeInDb = ledgerRepository.getContractAccountSet(block).getContract(contractDeployKey.getAddress()) | byte[] contractCodeInDb = ledgerRepository.getContractAccountSet(block).getContract(contractDeployKey.getAddress()) | ||||
| .getChainCode(); | .getChainCode(); | ||||
| assertArrayEquals(contractCode, contractCodeInDb); | assertArrayEquals(contractCode, contractCodeInDb); | ||||
| txContentHash = ptx.getHash(); | |||||
| // execute the contract; | // execute the contract; | ||||
| testContractExe(adminKey, ledgerHash, userKey, blockchainService, ledgerRepository, AssetContract.class); | |||||
| testContractExe(adminKey, ledgerHash, keyPairResponse.keyPair, blockchainService, ledgerRepository); | |||||
| return block; | return block; | ||||
| } | } | ||||
| private static <T> void testContractExe(AsymmetricKeypair adminKey, HashDigest ledgerHash, BlockchainKeypair userKey, | |||||
| BlockchainService blockchainService,LedgerRepository ledgerRepository,Class<T> contractIntf) { | |||||
| private static <T> void testContractExe(AsymmetricKeypair adminKey, HashDigest ledgerHash, BlockchainKeypair dataKey, | |||||
| BlockchainService blockchainService,LedgerRepository ledgerRepository) { | |||||
| LedgerInfo ledgerInfo = blockchainService.getLedger(ledgerHash); | LedgerInfo ledgerInfo = blockchainService.getLedger(ledgerHash); | ||||
| LedgerBlock previousBlock = blockchainService.getBlock(ledgerHash, ledgerInfo.getLatestBlockHeight() - 1); | LedgerBlock previousBlock = blockchainService.getBlock(ledgerHash, ledgerInfo.getLatestBlockHeight() - 1); | ||||
| // 定义交易; | // 定义交易; | ||||
| TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); | TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); | ||||
| Byte byteObj = Byte.parseByte("127"); | |||||
| Byte byteObj = Byte.parseByte("123"); | |||||
| // txTpl.contract(contractDeployKey.getAddress(),AssetContract2.class).issue(byteObj, | |||||
| // contractDeployKey.getAddress().toBase58(),321123); | |||||
| txTpl.contract(contractDeployKey.getAddress(),AssetContract2.class).issue(byteObj, | txTpl.contract(contractDeployKey.getAddress(),AssetContract2.class).issue(byteObj, | ||||
| contractDeployKey.getAddress().toBase58(),321123); | |||||
| dataKey.getAddress().toBase58(),Bytes.fromString("123321")); | |||||
| // txTpl.contract(contractDeployKey.getAddress(),AssetContract2.class).issue(byteObj,dataKey.getAddress().toBase58(),123456); | |||||
| // 签名; | // 签名; | ||||
| PreparedTransaction ptx = txTpl.prepare(); | PreparedTransaction ptx = txTpl.prepare(); | ||||
| @@ -493,8 +500,11 @@ public class IntegrationBase { | |||||
| TransactionResponse txResp = ptx.commit(); | TransactionResponse txResp = ptx.commit(); | ||||
| // 验证结果; | // 验证结果; | ||||
| txResp.getContentHash(); | |||||
| Assert.assertTrue(txResp.isSuccess()); | Assert.assertTrue(txResp.isSuccess()); | ||||
| assertEquals(ptx.getHash(),txResp.getContentHash()); | |||||
| LedgerBlock block = ledgerRepository.getBlock(txResp.getBlockHeight()); | |||||
| KVDataEntry[] kvDataEntries = ledgerRepository.getDataAccountSet(block).getDataAccount(dataKey.getAddress()).getDataEntries(0,1); | |||||
| assertEquals("100",kvDataEntries[0].getValue().toString()); | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -1,12 +1,12 @@ | |||||
| package test.com.jd.blockchain.intgr.contract; | package test.com.jd.blockchain.intgr.contract; | ||||
| import com.jd.blockchain.binaryproto.DataContract; | |||||
| import com.jd.blockchain.consts.DataCodes; | |||||
| import com.jd.blockchain.contract.Contract; | import com.jd.blockchain.contract.Contract; | ||||
| import com.jd.blockchain.contract.ContractEvent; | import com.jd.blockchain.contract.ContractEvent; | ||||
| import com.jd.blockchain.ledger.TransactionContentBody; | |||||
| import com.jd.blockchain.ledger.ContractBizContent; | |||||
| import com.jd.blockchain.utils.Bytes; | import com.jd.blockchain.utils.Bytes; | ||||
| import java.math.BigDecimal; | |||||
| /** | /** | ||||
| * 示例:一个“资产管理”智能合约; | * 示例:一个“资产管理”智能合约; | ||||
| * | * | ||||
| @@ -22,32 +22,23 @@ public interface AssetContract2 { | |||||
| * 新发行的资产的持有账户; | * 新发行的资产的持有账户; | ||||
| */ | */ | ||||
| @ContractEvent(name = "issue-asset-0") | @ContractEvent(name = "issue-asset-0") | ||||
| void issue(@DataContract(code = DataCodes.TX_CONTENT_BODY) TransactionContentBody transactionContentBody, | |||||
| @DataContract(code = DataCodes.CONTRACT_TEXT) String assetHolderAddress); | |||||
| void issue(ContractBizContent contractBizContent, String assetHolderAddress); | |||||
| /** | /** | ||||
| * issue asset; | |||||
| * @param transactionContentBody | |||||
| * 发行资产; | |||||
| * 新发行的资产数量; | |||||
| * @param assetHolderAddress | * @param assetHolderAddress | ||||
| * @param cashNumber | |||||
| * 新发行的资产的持有账户; | |||||
| */ | */ | ||||
| @ContractEvent(name = "issue-asset") | @ContractEvent(name = "issue-asset") | ||||
| public void issue(@DataContract(code = DataCodes.TX_CONTENT_BODY) TransactionContentBody transactionContentBody, | |||||
| @DataContract(code = DataCodes.CONTRACT_TEXT) String assetHolderAddress, | |||||
| @DataContract(code = DataCodes.CONTRACT_INT64) long cashNumber); | |||||
| void issue(ContractBizContent contractBizContent, String assetHolderAddress, long cashNumber); | |||||
| /** | |||||
| * Bytes can bring the byte[]; | |||||
| * @param bytes | |||||
| * @param assetHolderAddress | |||||
| * @param cashNumber | |||||
| */ | |||||
| @ContractEvent(name = "issue-asset-2") | @ContractEvent(name = "issue-asset-2") | ||||
| void issue(Bytes bytes, String assetHolderAddress, long cashNumber); | void issue(Bytes bytes, String assetHolderAddress, long cashNumber); | ||||
| @ContractEvent(name = "issue-asset-3") | |||||
| void issue(Byte bytes, String assetHolderAddress, long cashNumber); | |||||
| @ContractEvent(name = "issue-asset-3") | |||||
| void issue(Byte byteObj, String assetHolderAddress, long cashNumber); | |||||
| @ContractEvent(name = "issue-asset-4") | @ContractEvent(name = "issue-asset-4") | ||||
| void issue1(byte[] bytes, String assetHolderAddress, long cashNumber); | |||||
| void issue(Byte byteObj, String assetHolderAddress, Bytes cashNumber); | |||||
| } | } | ||||