Compare commits

..

No commits in common. "charlesmchen/mobileCoinMinimal" and "master" have entirely different histories.

318 changed files with 26106 additions and 2056 deletions

4
.gitmodules vendored
View File

@ -0,0 +1,4 @@
[submodule "Vendor/libmobilecoin-ios-artifacts"]
path = Vendor/libmobilecoin-ios-artifacts
url = https://github.com/mobilecoinofficial/libmobilecoin-ios-artifacts.git
shallow = true

View File

@ -0,0 +1,514 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
2706868D2474C4D800B82C57 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2706868C2474C4D800B82C57 /* AppDelegate.swift */; };
270686932474C4DA00B82C57 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 270686922474C4DA00B82C57 /* Assets.xcassets */; };
270686962474C4DA00B82C57 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 270686952474C4DA00B82C57 /* Preview Assets.xcassets */; };
270686992474C4DA00B82C57 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 270686972474C4DA00B82C57 /* LaunchScreen.storyboard */; };
27A1FABF24E8B89C001A0614 /* Performance Tests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 27A1FABE24E8B89C001A0614 /* Performance Tests.xctestplan */; };
F9800F2C17E6D5B2B021EB8D /* Pods_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C0382FE8E8E7AAAC02CECA3 /* Pods_Example.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
05E35D1FAC2827984EE8B421 /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = "<group>"; };
270686892474C4D800B82C57 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
2706868C2474C4D800B82C57 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
270686922474C4DA00B82C57 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
270686952474C4DA00B82C57 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
270686982474C4DA00B82C57 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
2706869A2474C4DA00B82C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
27A1FABE24E8B89C001A0614 /* Performance Tests.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Performance Tests.xctestplan"; sourceTree = "<group>"; };
27A39C3C24C2622600B44786 /* Integration Tests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Integration Tests.xctestplan"; sourceTree = "<group>"; };
27A39C3D24C2622600B44786 /* Unit Tests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Unit Tests.xctestplan"; sourceTree = "<group>"; };
3AF40DB60A0ADA2CB879AE38 /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = "<group>"; };
4C0382FE8E8E7AAAC02CECA3 /* Pods_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FC106CAA4E993C2D6301C5CA /* Pods-Example.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.testable release.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.testable release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
270686862474C4D800B82C57 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F9800F2C17E6D5B2B021EB8D /* Pods_Example.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
270686802474C4D800B82C57 = {
isa = PBXGroup;
children = (
2706868B2474C4D800B82C57 /* Example */,
27A39C3B24C2622600B44786 /* TestPlans */,
2706868A2474C4D800B82C57 /* Products */,
4256C0AB87AF2BC2F24F1B92 /* Pods */,
9E89A7B3975ECA671C9D8346 /* Frameworks */,
);
sourceTree = "<group>";
};
2706868A2474C4D800B82C57 /* Products */ = {
isa = PBXGroup;
children = (
270686892474C4D800B82C57 /* Example.app */,
);
name = Products;
sourceTree = "<group>";
};
2706868B2474C4D800B82C57 /* Example */ = {
isa = PBXGroup;
children = (
2706868C2474C4D800B82C57 /* AppDelegate.swift */,
270686922474C4DA00B82C57 /* Assets.xcassets */,
270686972474C4DA00B82C57 /* LaunchScreen.storyboard */,
2706869A2474C4DA00B82C57 /* Info.plist */,
270686942474C4DA00B82C57 /* Preview Content */,
);
path = Example;
sourceTree = "<group>";
};
270686942474C4DA00B82C57 /* Preview Content */ = {
isa = PBXGroup;
children = (
270686952474C4DA00B82C57 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
27A39C3B24C2622600B44786 /* TestPlans */ = {
isa = PBXGroup;
children = (
27A39C3D24C2622600B44786 /* Unit Tests.xctestplan */,
27A39C3C24C2622600B44786 /* Integration Tests.xctestplan */,
27A1FABE24E8B89C001A0614 /* Performance Tests.xctestplan */,
);
path = TestPlans;
sourceTree = "<group>";
};
4256C0AB87AF2BC2F24F1B92 /* Pods */ = {
isa = PBXGroup;
children = (
05E35D1FAC2827984EE8B421 /* Pods-Example.debug.xcconfig */,
3AF40DB60A0ADA2CB879AE38 /* Pods-Example.release.xcconfig */,
FC106CAA4E993C2D6301C5CA /* Pods-Example.testable release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9E89A7B3975ECA671C9D8346 /* Frameworks */ = {
isa = PBXGroup;
children = (
4C0382FE8E8E7AAAC02CECA3 /* Pods_Example.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
270686882474C4D800B82C57 /* Example */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2706869D2474C4DA00B82C57 /* Build configuration list for PBXNativeTarget "Example" */;
buildPhases = (
A960891E414B6399548D3852 /* [CP] Check Pods Manifest.lock */,
270686852474C4D800B82C57 /* Sources */,
270686862474C4D800B82C57 /* Frameworks */,
270686872474C4D800B82C57 /* Resources */,
1851CDD4271ED50E0D6D8B7B /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Example;
productName = Example;
productReference = 270686892474C4D800B82C57 /* Example.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
270686812474C4D800B82C57 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1250;
ORGANIZATIONNAME = "MobileCoin Inc.";
TargetAttributes = {
270686882474C4D800B82C57 = {
CreatedOnToolsVersion = 11.4.1;
};
};
};
buildConfigurationList = 270686842474C4D800B82C57 /* Build configuration list for PBXProject "Example" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 270686802474C4D800B82C57;
productRefGroup = 2706868A2474C4D800B82C57 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
270686882474C4D800B82C57 /* Example */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
270686872474C4D800B82C57 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
270686992474C4DA00B82C57 /* LaunchScreen.storyboard in Resources */,
27A1FABF24E8B89C001A0614 /* Performance Tests.xctestplan in Resources */,
270686962474C4DA00B82C57 /* Preview Assets.xcassets in Resources */,
270686932474C4DA00B82C57 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1851CDD4271ED50E0D6D8B7B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
A960891E414B6399548D3852 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Example-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
270686852474C4D800B82C57 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2706868D2474C4D800B82C57 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
270686972474C4DA00B82C57 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
270686982474C4DA00B82C57 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
2706869B2474C4DA00B82C57 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = NO;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
2706869C2474C4DA00B82C57 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
2706869E2474C4DA00B82C57 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 05E35D1FAC2827984EE8B421 /* Pods-Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
DEVELOPMENT_TEAM = 8JT9JJD9Y5;
ENABLE_ONLY_ACTIVE_RESOURCES = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Example/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.mobilecoin.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
2706869F2474C4DA00B82C57 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3AF40DB60A0ADA2CB879AE38 /* Pods-Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
DEVELOPMENT_TEAM = 8JT9JJD9Y5;
ENABLE_ONLY_ACTIVE_RESOURCES = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Example/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mobilecoin.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
274D3FAD24E8B5DD004E2F4A /* Testable Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = "Testable Release";
};
274D3FAE24E8B5DD004E2F4A /* Testable Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FC106CAA4E993C2D6301C5CA /* Pods-Example.testable release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
DEVELOPMENT_TEAM = 8JT9JJD9Y5;
ENABLE_ONLY_ACTIVE_RESOURCES = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Example/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mobilecoin.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = "Testable Release";
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
270686842474C4D800B82C57 /* Build configuration list for PBXProject "Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2706869B2474C4DA00B82C57 /* Debug */,
2706869C2474C4DA00B82C57 /* Release */,
274D3FAD24E8B5DD004E2F4A /* Testable Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2706869D2474C4DA00B82C57 /* Build configuration list for PBXNativeTarget "Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2706869E2474C4DA00B82C57 /* Debug */,
2706869F2474C4DA00B82C57 /* Release */,
274D3FAE24E8B5DD004E2F4A /* Testable Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 270686812474C4D800B82C57 /* Project object */;
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Example.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FILEHEADER</key>
<string>
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//</string>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "45F542481CBFF9E786B4F9148AC9D6F2"
BuildableName = "MobileCoin-Unit-Core-IntegrationTests.xctest"
BlueprintName = "MobileCoin-Unit-Core-IntegrationTests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:TestPlans/Integration Tests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "45F542481CBFF9E786B4F9148AC9D6F2"
BuildableName = "MobileCoin-Unit-Core-IntegrationTests.xctest"
BlueprintName = "MobileCoin-Unit-Core-IntegrationTests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Testable Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E8A0545E992AC3A0A6B313DAF029D326"
BuildableName = "MobileCoin-UI-Core-PerformanceTests.xctest"
BlueprintName = "MobileCoin-UI-Core-PerformanceTests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Testable Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:TestPlans/Performance Tests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E8A0545E992AC3A0A6B313DAF029D326"
BuildableName = "MobileCoin-UI-Core-PerformanceTests.xctest"
BlueprintName = "MobileCoin-UI-Core-PerformanceTests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Testable Release">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Testable Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C10663274EBBCA6839AB2898903FF5CB"
BuildableName = "MobileCoin-Unit-Core-Tests.xctest"
BlueprintName = "MobileCoin-Unit-Core-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:TestPlans/Unit Tests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C10663274EBBCA6839AB2898903FF5CB"
BuildableName = "MobileCoin-Unit-Core-Tests.xctest"
BlueprintName = "MobileCoin-Unit-Core-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Testable Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,16 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
}

View File

@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

7
Example/Gemfile Normal file
View File

@ -0,0 +1,7 @@
source 'https://rubygems.org' do
gem 'cocoapods', '~> 1.11'
gem 'cocoapods-binary', :git => 'https://github.com/mobilecoinofficial/cocoapods-binary.git', :tag => 'v0.4.4.rev5'
gem 'cocoapods-repo-update'
gem 'cocoapods-keys'
gem 'fastlane'
end

306
Example/Gemfile.lock Normal file
View File

@ -0,0 +1,306 @@
GIT
remote: https://github.com/mobilecoinofficial/cocoapods-binary.git
revision: 748a64cc890cfaee59cdd67e80d9ade82b1b179b
tag: v0.4.4.rev5
specs:
cocoapods-binary (0.4.4)
cocoapods (>= 1.5.0, < 2.0)
fourflusher (~> 2.0)
xcpretty (~> 0.3.0)
GEM
specs:
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.4)
rexml
RubyInline (3.12.5)
ZenTest (~> 4.3)
ZenTest (4.12.0)
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.520.0)
aws-sdk-core (3.121.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.50.0)
aws-sdk-core (~> 3, >= 3.121.2)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.104.0)
aws-sdk-core (~> 3, >= 3.121.2)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.0.3)
cocoapods (1.11.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.2)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.5.1)
cocoapods-keys (2.2.1)
dotenv
osx_keychain
cocoapods-plugins (1.0.0)
nap
cocoapods-repo-update (0.0.4)
cocoapods (~> 1.0, >= 1.3.0)
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.9)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.88.0)
faraday (1.8.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.5)
fastlane (2.197.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
ffi (1.15.4)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.13.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.8.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.6.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.9.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.34.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
jmespath (1.4.0)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.14.4)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
optparse (0.1.1)
os (1.1.1)
osx_keychain (1.0.2)
RubyInline (~> 3)
plist (3.6.0)
public_suffix (4.0.6)
rake (13.0.6)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.0)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.1)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
zeitwerk (2.5.1)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (~> 1.11)!
cocoapods-binary!
cocoapods-keys!
cocoapods-repo-update!
fastlane!
BUNDLED WITH
2.2.24

43
Example/Makefile Normal file
View File

@ -0,0 +1,43 @@
.PHONY: default
default: setup bootstrap build test
.PHONY: setup
setup:
bundle install
.PHONY: bootstrap
bootstrap:
bundle exec pod install
.PHONY: build
build:
bundle exec fastlane gym \
--scheme "Unit Tests" \
--skip_archive \
--skip_codesigning
bundle exec fastlane gym \
--scheme "Integration Tests" \
--skip_archive \
--skip_codesigning
bundle exec fastlane gym \
--scheme "Performance Tests" \
--skip_archive \
--skip_codesigning
.PHONY: test
test:
bundle exec fastlane scan \
--scheme "Unit Tests"
bundle exec fastlane scan \
--scheme "Performance Tests"
.PHONY: clean
clean:
@[ ! -e test_output ] || rm -r test_output
# Maintenance commands
.PHONY: upgrade-deps
upgrade-deps:
bundle update
bundle exec pod update

92
Example/Podfile Normal file
View File

@ -0,0 +1,92 @@
source 'https://cdn.cocoapods.org/'
platform :ios, '10.0'
plugin 'cocoapods-repo-update'
keep_source_code_for_prebuilt_frameworks!
plugin 'cocoapods-keys', {
:project => "MobileCoin",
:keys => [
"devNetworkAuthUsername",
"devNetworkAuthPassword",
"testNetTestAccountMnemonicsCommaSeparated",
]}
use_frameworks!
ENV['MC_ENABLE_SWIFTLINT_SCRIPT'] = '1'
ENV['MC_ENABLE_WARN_LONG_COMPILE_TIMES'] = '1'
target 'Example' do
pod 'MobileCoin', path: '..'
pod 'MobileCoin/Core', path: '..', testspecs: ['Tests', 'IntegrationTests', 'PerformanceTests']
# pod 'MobileCoin', podspec: '../MobileCoin.podspec'
# pod 'MobileCoin/Core', podspec: '../MobileCoin.podspec', testspecs: ['Tests', 'IntegrationTests']
# pod 'MobileCoin', git: 'https://github.com/mobilecoinofficial/MobileCoin-Swift.git'
# pod 'MobileCoin/Core', git: 'https://github.com/mobilecoinofficial/MobileCoin-Swift.git', testspecs: ['Tests', 'IntegrationTests']
pod 'LibMobileCoin'
# pod 'LibMobileCoin', path: '../Vendor/libmobilecoin-ios-artifacts'
# pod 'LibMobileCoin', podspec: '../Vendor/libmobilecoin-ios-artifacts/LibMobileCoin.podspec'
# pod 'LibMobileCoin', git: 'https://github.com/the-real-adammork/libmobilecoin-ios-artifacts.git'
pod 'gRPC-Swift'
pod 'SwiftProtobuf'
pod 'SwiftLint'
end
post_install do |installer|
# Enable building tests using Testable Release build configuration
installer.pods_project.targets.each do |target|
next unless target.name == 'MobileCoin'
target.build_configurations.each do |config|
next unless config.name == 'Testable Release'
config.build_settings['ENABLE_TESTABILITY'] = 'YES'
end
end
# Enable running performance tests on a physical device
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if target.name == 'AppHost-MobileCoin-UI-Tests'
config.build_settings['DEVELOPMENT_TEAM'] = '8JT9JJD9Y5'
elsif target.name.start_with? 'MobileCoin-UI-'
config.build_settings['DEVELOPMENT_TEAM'] = '8JT9JJD9Y5'
config.build_settings.delete('CODE_SIGN_IDENTITY[sdk=iphoneos*]')
end
end
end
# Bump deployment target to 9.0 for pods that explicitly specify 8.0, since 9.0 is the minimum
# that Xcode 12 supports.
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']) < Gem::Version.new('9.0')
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
end
end
end
# Add Keys framework to Integration Tests for injecting values using cocoapods-keys
installer.pods_project.targets.each do |target|
next unless target.name == 'MobileCoin-Unit-Core-IntegrationTests'
installer.pods_project.targets.each do |keys_target|
next unless keys_target.name == 'Keys'
target.add_dependency(keys_target)
end
target.build_configurations.each do |config|
config.build_settings["FRAMEWORK_SEARCH_PATHS"] ||= "$(inherited)"
config.build_settings["FRAMEWORK_SEARCH_PATHS"] << ' "${PODS_CONFIGURATION_BUILD_DIR}/Keys"'
config.build_settings["OTHER_LDFLAGS"] ||= "$(inherited)"
config.build_settings["OTHER_LDFLAGS"] << ' -framework "Keys"'
end
end
# Disable bitcode on test targets
installer.pods_project.targets.each do |target|
next unless ['MobileCoin-Unit-Core-IntegrationTests', 'MobileCoin-UI-Core-PerformanceTests', 'MobileCoin-Unit-Core-Tests'].find_index(target.name) != nil
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end

204
Example/Podfile.lock Normal file
View File

@ -0,0 +1,204 @@
PODS:
- _NIODataStructures (2.32.3)
- CGRPCZlib (1.5.0)
- CNIOAtomics (2.32.3)
- CNIOBoringSSL (2.15.1)
- CNIOBoringSSLShims (2.15.1):
- CNIOBoringSSL (= 2.15.1)
- CNIODarwin (2.32.3)
- CNIOHTTPParser (2.32.3)
- CNIOLinux (2.32.3)
- CNIOWindows (2.32.3)
- gRPC-Swift (1.5.0):
- CGRPCZlib (= 1.5.0)
- Logging (< 2.0.0, >= 1.4.0)
- SwiftNIO (< 3.0.0, >= 2.32.0)
- SwiftNIOExtras (< 2.0.0, >= 1.4.0)
- SwiftNIOHTTP2 (< 2.0.0, >= 1.18.2)
- SwiftNIOSSL (< 3.0.0, >= 2.14.0)
- SwiftNIOTransportServices (< 2.0.0, >= 1.11.1)
- SwiftProtobuf (< 2.0.0, >= 1.9.0)
- Keys (1.0.1)
- LibMobileCoin (1.2.0-pre3):
- gRPC-Swift
- SwiftProtobuf (~> 1.5)
- Logging (1.4.0)
- MobileCoin (1.2.0-pre2):
- MobileCoin/Core (= 1.2.0-pre2)
- MobileCoin/Core (1.2.0-pre2):
- gRPC-Swift
- LibMobileCoin (~> 1.2.0-pre3)
- Logging (~> 1.4)
- SwiftLint
- SwiftNIO
- SwiftNIOHPACK
- SwiftNIOHTTP1
- SwiftProtobuf
- MobileCoin/Core/IntegrationTests (1.2.0-pre2):
- gRPC-Swift
- LibMobileCoin (~> 1.2.0-pre3)
- Logging (~> 1.4)
- SwiftLint
- SwiftNIO
- SwiftNIOHPACK
- SwiftNIOHTTP1
- SwiftProtobuf
- MobileCoin/Core/PerformanceTests (1.2.0-pre2):
- gRPC-Swift
- LibMobileCoin (~> 1.2.0-pre3)
- Logging (~> 1.4)
- SwiftLint
- SwiftNIO
- SwiftNIOHPACK
- SwiftNIOHTTP1
- SwiftProtobuf
- MobileCoin/Core/Tests (1.2.0-pre2):
- gRPC-Swift
- LibMobileCoin (~> 1.2.0-pre3)
- Logging (~> 1.4)
- SwiftLint
- SwiftNIO
- SwiftNIOHPACK
- SwiftNIOHTTP1
- SwiftProtobuf
- SwiftLint (0.45.0)
- SwiftNIO (2.32.3):
- SwiftNIOCore (= 2.32.3)
- SwiftNIOEmbedded (= 2.32.3)
- SwiftNIOPosix (= 2.32.3)
- SwiftNIOConcurrencyHelpers (2.32.3):
- CNIOAtomics (= 2.32.3)
- SwiftNIOCore (2.32.3):
- CNIOLinux (= 2.32.3)
- SwiftNIOConcurrencyHelpers (= 2.32.3)
- SwiftNIOEmbedded (2.32.3):
- _NIODataStructures (= 2.32.3)
- SwiftNIOCore (= 2.32.3)
- SwiftNIOExtras (1.10.2):
- SwiftNIO (< 3, >= 2.32.0)
- SwiftNIOFoundationCompat (2.32.3):
- SwiftNIO (= 2.32.3)
- SwiftNIOCore (= 2.32.3)
- SwiftNIOHPACK (1.18.3):
- SwiftNIO (< 3, >= 2.32.0)
- SwiftNIOConcurrencyHelpers (< 3, >= 2.32.0)
- SwiftNIOCore (< 3, >= 2.32.0)
- SwiftNIOHTTP1 (< 3, >= 2.32.0)
- SwiftNIOHTTP1 (2.32.3):
- CNIOHTTPParser (= 2.32.3)
- SwiftNIO (= 2.32.3)
- SwiftNIOConcurrencyHelpers (= 2.32.3)
- SwiftNIOCore (= 2.32.3)
- SwiftNIOHTTP2 (1.18.3):
- SwiftNIO (< 3, >= 2.32.0)
- SwiftNIOConcurrencyHelpers (< 3, >= 2.32.0)
- SwiftNIOCore (< 3, >= 2.32.0)
- SwiftNIOHPACK (= 1.18.3)
- SwiftNIOHTTP1 (< 3, >= 2.32.0)
- SwiftNIOTLS (< 3, >= 2.32.0)
- SwiftNIOPosix (2.32.3):
- _NIODataStructures (= 2.32.3)
- CNIODarwin (= 2.32.3)
- CNIOLinux (= 2.32.3)
- CNIOWindows (= 2.32.3)
- SwiftNIOConcurrencyHelpers (= 2.32.3)
- SwiftNIOCore (= 2.32.3)
- SwiftNIOSSL (2.15.1):
- CNIOBoringSSL (= 2.15.1)
- CNIOBoringSSLShims (= 2.15.1)
- SwiftNIO (< 3, >= 2.32.0)
- SwiftNIOConcurrencyHelpers (< 3, >= 2.32.0)
- SwiftNIOCore (< 3, >= 2.32.0)
- SwiftNIOTLS (< 3, >= 2.32.0)
- SwiftNIOTLS (2.32.3):
- SwiftNIO (= 2.32.3)
- SwiftNIOCore (= 2.32.3)
- SwiftNIOTransportServices (1.11.3):
- SwiftNIO (< 3, >= 2.32.0)
- SwiftNIOConcurrencyHelpers (< 3, >= 2.32.0)
- SwiftNIOFoundationCompat (< 3, >= 2.32.0)
- SwiftNIOTLS (< 3, >= 2.32.0)
- SwiftProtobuf (1.18.0)
DEPENDENCIES:
- gRPC-Swift
- Keys (from `Pods/CocoaPodsKeys`)
- LibMobileCoin
- MobileCoin (from `..`)
- MobileCoin/Core (from `..`)
- MobileCoin/Core/IntegrationTests (from `..`)
- MobileCoin/Core/PerformanceTests (from `..`)
- MobileCoin/Core/Tests (from `..`)
- SwiftLint
- SwiftProtobuf
SPEC REPOS:
trunk:
- _NIODataStructures
- CGRPCZlib
- CNIOAtomics
- CNIOBoringSSL
- CNIOBoringSSLShims
- CNIODarwin
- CNIOHTTPParser
- CNIOLinux
- CNIOWindows
- gRPC-Swift
- LibMobileCoin
- Logging
- SwiftLint
- SwiftNIO
- SwiftNIOConcurrencyHelpers
- SwiftNIOCore
- SwiftNIOEmbedded
- SwiftNIOExtras
- SwiftNIOFoundationCompat
- SwiftNIOHPACK
- SwiftNIOHTTP1
- SwiftNIOHTTP2
- SwiftNIOPosix
- SwiftNIOSSL
- SwiftNIOTLS
- SwiftNIOTransportServices
- SwiftProtobuf
EXTERNAL SOURCES:
Keys:
:path: Pods/CocoaPodsKeys
MobileCoin:
:path: ".."
SPEC CHECKSUMS:
_NIODataStructures: e2077c7dc7c1d6c93e698c85fe04d663a17f53a4
CGRPCZlib: db324e4e4e71262d48faceb86b52dd7d4f71ff62
CNIOAtomics: 4dde57e1838a29a9b23ef91617505f34751cdbe5
CNIOBoringSSL: c99129423da079a9eb74bcfc7cfec41a6775cf94
CNIOBoringSSLShims: 902ae35fea0b6be5eefb4fdce906751886cfa46f
CNIODarwin: 0489511f8486443af71ff986ccd5abbc680ae713
CNIOHTTPParser: f7a6816f7ddbe7dfa57a74cd36dc2db2c53b56e8
CNIOLinux: 5921dfefbc4bbe017380b34c510855622147ea41
CNIOWindows: f5aa9dfb401b440a7b4c9cd911e53e981a787193
gRPC-Swift: 8942047451bf81413077e6b94bf4213e47b181cc
Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
LibMobileCoin: f3c44fb94b8647bd5cac3aedf0b1ca24f6cc7201
Logging: beeb016c9c80cf77042d62e83495816847ef108b
MobileCoin: 339e51b8739b7ea2f0519cc7bdde8c4e05c2b374
SwiftLint: e5c7f1fba68eccfc51509d5b2ce1699f5502e0c7
SwiftNIO: bb336ceef32850e9671d3fa0e0cc2b9add3b5948
SwiftNIOConcurrencyHelpers: ca2594e10749655f42baf5468212be83d2f94fe3
SwiftNIOCore: 9deed6620f80c7c82e8e2c2ffb9864495416d892
SwiftNIOEmbedded: b7ccf12b402dff35a5d4356990a6253621e4337d
SwiftNIOExtras: 70f09aa8eca3ab6baeaf1993da9c855b6e95e97f
SwiftNIOFoundationCompat: d3b888766e7c67354a4e4e145d38edf9586efa0c
SwiftNIOHPACK: e2fc784ce453bec4c058b21071e89fb7e542ac30
SwiftNIOHTTP1: 349a16aae363250cd49f430a9fdb93cff518adfa
SwiftNIOHTTP2: a0322f3dcecd949e03df65f4dac106411df0f12c
SwiftNIOPosix: e4988a8dcfd5a6319bde219d7a3d0acc5fbe7a89
SwiftNIOSSL: 7c2ddcbcbb2a8188468b7fe9c2bc6124df4b3772
SwiftNIOTLS: 1b8290ec775238ccc714ed842d929494df95a2c2
SwiftNIOTransportServices: 1fbbdb58510af3c53a838a1dbea98f18132dc952
SwiftProtobuf: c3c12645230d9b09c72267e0de89468c5543bd86
PODFILE CHECKSUM: 46e25fe8f13c4beb3ac1e4ab1a9512960701d05d
COCOAPODS: 1.11.2

View File

@ -0,0 +1,30 @@
{
"configurations" : [
{
"id" : "7B8CC01D-B029-4322-84A2-F7F086835547",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
},
"testTargets" : [
{
"skippedTests" : [
"ConsensusConnectionIntTests\/testWrongTrustRootFails()",
"FogBlockConnectionIntTests\/testDoSGetBlocks()",
"FogBlockConnectionIntTests\/testGetBlockZero()",
"MobileCoinClientPublicApiIntTests\/testWrongConsensusTrustRootReturnsError()"
],
"target" : {
"containerPath" : "container:Pods\/Pods.xcodeproj",
"identifier" : "45F542481CBFF9E786B4F9148AC9D6F2",
"name" : "MobileCoin-Unit-Core-IntegrationTests"
}
}
],
"version" : 1
}

View File

@ -0,0 +1,25 @@
{
"configurations" : [
{
"id" : "98A4F1B1-FE76-43E0-BD61-A2B0B7F719FC",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
"uiTestingScreenshotsLifetime" : "keepNever",
"userAttachmentLifetime" : "keepNever"
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:Pods\/Pods.xcodeproj",
"identifier" : "E8A0545E992AC3A0A6B313DAF029D326",
"name" : "MobileCoin-UI-Core-PerformanceTests"
}
}
],
"version" : 1
}

View File

@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "E10088A3-9143-42A3-8186-B64A2E756B6A",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:Pods\/Pods.xcodeproj",
"identifier" : "C10663274EBBCA6839AB2898903FF5CB",
"name" : "MobileCoin-Unit-Core-Tests"
}
}
],
"version" : 1
}

4
Gemfile Normal file
View File

@ -0,0 +1,4 @@
source 'https://rubygems.org' do
gem 'cocoapods', '~> 1.11'
gem 'jazzy'
end

121
Gemfile.lock Normal file
View File

@ -0,0 +1,121 @@
GEM
specs:
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.4)
rexml
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
claide (1.0.3)
cocoapods (1.11.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.2)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.5.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.1.9)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
ffi (1.15.4)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
jazzy (0.14.1)
cocoapods (~> 1.5)
mustache (~> 1.1)
open4 (~> 1.3)
redcarpet (~> 3.4)
rexml (~> 3.2)
rouge (>= 2.0.6, < 4.0)
sassc (~> 2.1)
sqlite3 (~> 1.3)
xcinvoke (~> 0.3.0)
json (2.6.1)
liferaft (0.0.6)
minitest (5.14.4)
molinillo (0.8.0)
mustache (1.1.1)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
open4 (1.3.4)
public_suffix (4.0.6)
redcarpet (3.5.1)
rexml (3.2.5)
rouge (3.26.1)
ruby-macho (2.5.1)
sassc (2.4.0)
ffi (~> 1.9)
sqlite3 (1.4.2)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
xcinvoke (0.3.0)
liferaft (~> 0.0.6)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
zeitwerk (2.5.1)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (~> 1.11)!
jazzy!
BUNDLED WITH
2.2.24

View File

@ -1,7 +0,0 @@
#import <CocoaLumberjack/CocoaLumberjack.h>
#ifdef DEBUG
static const NSUInteger ddLogLevel = DDLogLevelAll;
#else
static const NSUInteger ddLogLevel = DDLogLevelInfo;
#endif

View File

@ -1,34 +0,0 @@
import Foundation
public enum MobileCoinMinimalError: Error {
case invalidReceipt
}
// MARK: -
public class MobileCoinMinimal {
public static func txOutPublicKey(forReceiptData serializedData: Data) throws -> Data {
guard let proto = try? External_Receipt(serializedData: serializedData) else {
logger.warning(
"External_Receipt deserialization failed. serializedData: " +
"\(redacting: serializedData.base64EncodedString())",
logFunction: false)
throw MobileCoinMinimalError.invalidReceipt
}
let txOutPublicKey = proto.publicKey.data
return txOutPublicKey
}
public static func isValidMobileCoinPublicAddress(_ serializedData: Data) -> Bool {
guard let proto = try? External_PublicAddress(serializedData: serializedData) else {
logger.warning("External_PublicAddress deserialization failed. serializedData: " +
"\(redacting: serializedData.base64EncodedString())")
return false
}
return true
}
}

File diff suppressed because it is too large Load Diff

104
Makefile Normal file
View File

@ -0,0 +1,104 @@
.PHONY: default
default: setup bootstrap build test
# Commands
.PHONY: setup
setup:
bundle install
@$(MAKE) --directory=Example setup
.PHONY: bootstrap
bootstrap:
@$(MAKE) --directory=Example bootstrap
.PHONY: build
build:
@$(MAKE) --directory=Example build
.PHONY: test
test:
@$(MAKE) --directory=Example test
.PHONY: clean
clean: clean-docs
@$(MAKE) --directory=Example clean
.PHONY: lint
lint: swiftlint
.PHONY: lint-all
lint-all: lint lint-circleci lint-podspec lint-docs
.PHONY: publish
publish: tag-release publish-podspec
# Release
.PHONY: tag-release
tag-release:
VERSION="$$(bundle exec pod ipc spec MobileCoin.podspec | jq -r '.version')" && \
git tag "v$$VERSION" && \
git push git@github.com:mobilecoinofficial/MobileCoin-Swift.git "refs/tags/v$$VERSION"
# MobileCoin pod
.PHONY: lint-podspec
lint-podspec:
bundle exec pod spec lint MobileCoin.podspec --skip-tests
.PHONY: publish-podspec
publish-podspec:
bundle exec pod trunk push MobileCoin.podspec --skip-tests
# CircleCI
.PHONY: install-circleci
install-circleci:
brew install circleci
.PHONY: lint-circleci
lint-circleci:
@command -v circleci >/dev/null || $(MAKE) install-circleci
circleci config validate
# Documentation
.PHONY: docs
docs:
bundle exec jazzy
.PHONY: clean-docs
clean-docs:
@[ ! -e docs ] || rm -r docs
.PHONY: lint-docs
lint-docs:
@[ -e docs ] || $(MAKE) docs
@# Check that there are no categories that start with `Other `, since that signifies that a new public
@# type was added but was not added to a category in `.jazzy.yaml`
@[[ "$$( \
name_regex='^Other (?:Classes|Constants|Enumerations|Extensions|Functions|Protocols|Structures|Type Aliases|Type Definitions)$$'; \
cat docs/search.json | jq ".[] \
| select(has(\"parent_name\") | not) \
| select(has(\"name\")) \
| select(.name | test(\"$$name_regex\"))" \
)" == "" ]] || { echo 'Error: Found one or more public types not categorized in jazzy.'; exit 1; }
# Swiftlint
.PHONY: autocorrect
autocorrect:
@PATH="./Example/Pods/SwiftLint:$$PATH" swiftlint autocorrect
.PHONY: swiftlint
swiftlint:
@PATH="./Example/Pods/SwiftLint:$$PATH" swiftlint
# Maintenance
.PHONY: upgrade-deps
upgrade-deps:
bundle update
$(MAKE) -C Example upgrade-deps

View File

@ -2,7 +2,7 @@ Pod::Spec.new do |s|
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
s.name = "MobileCoinMinimal"
s.name = "MobileCoin"
s.version = "1.2.0-pre2"
s.summary = "A library for communicating with MobileCoin network"
@ -25,26 +25,48 @@ Pod::Spec.new do |s|
# ――― Subspecs ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
s.source_files = [
"Sources/**/*.{h,m,swift}",
"LibMobileCoin/**/*.{h,m,swift}",
"Glue/**/*.{h,m,swift}",
]
s.default_subspec = "Core"
s.subspec "Core" do |subspec|
subspec.source_files = [
"Sources/**/*.{h,m,swift}",
]
subspec.dependency "LibMobileCoin", "~> 1.2.0-pre3"
subspec.dependency "gRPC-Swift"
subspec.dependency "Logging", "~> 1.4"
subspec.dependency "SwiftNIO"
subspec.dependency "SwiftNIOHPACK"
subspec.dependency "SwiftNIOHTTP1"
subspec.dependency "SwiftProtobuf"
subspec.test_spec do |test_spec|
test_spec.source_files = "Tests/{Unit,Common}/**/*.swift"
test_spec.resources = [
"Tests/Common/FixtureData/**/*",
"Vendor/libmobilecoin-ios-artifacts/Vendor/mobilecoin/test-vectors/vectors/**/*",
]
end
subspec.test_spec 'IntegrationTests' do |test_spec|
test_spec.source_files = "Tests/{Integration,Common}/**/*.swift"
test_spec.resource = "Tests/Common/FixtureData/**/*"
end
subspec.test_spec 'PerformanceTests' do |test_spec|
test_spec.source_files = "Tests/{Performance,Common}/**/*.swift"
test_spec.test_type = :ui
test_spec.requires_app_host = true
end
unless ENV["MC_ENABLE_SWIFTLINT_SCRIPT"].nil?
subspec.dependency 'SwiftLint'
end
end
s.dependency "Logging", "~> 1.4"
s.dependency "SwiftProtobuf"
s.dependency "CocoaLumberjack"
# s.test_spec do |test_spec|
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = [
'Tests/**/*.{h,m,swift}',
'TestGlue/**/*.{h,m,swift}',
]
# test_spec.resources = 'Tests/**/*.{json,encrypted,webp}'
test_spec.resources = 'Tests/**/*.*'
end
# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
s.swift_version = "5.2"
@ -70,5 +92,23 @@ Pod::Spec.new do |s|
end
s.pod_target_xcconfig = pod_target_xcconfig
unless ENV["MC_ENABLE_SWIFTLINT_SCRIPT"].nil?
s.script_phases = [
{
:name => "Run SwiftLint",
:execution_position => :any,
:script => <<~'EOS'
SWIFTLINT="${PODS_ROOT}/SwiftLint/swiftlint"
if which ${SWIFTLINT} >/dev/null; then
cd "${PODS_TARGET_SRCROOT}"
${SWIFTLINT}
else
echo "warning: SwiftLint not installed, run \`pod install\`"
fi
EOS
},
]
end
end

View File

@ -0,0 +1,128 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
extension Account {
struct BalanceUpdater {
private let serialQueue: DispatchQueue
private let account: ReadWriteDispatchLock<Account>
private let txOutFetcher: FogView.TxOutFetcher
private let viewKeyScanner: FogViewKeyScanner
private let fogKeyImageChecker: FogKeyImageChecker
init(
account: ReadWriteDispatchLock<Account>,
fogViewService: FogViewService,
fogKeyImageService: FogKeyImageService,
fogBlockService: FogBlockService,
fogQueryScalingStrategy: FogQueryScalingStrategy,
targetQueue: DispatchQueue?
) {
self.serialQueue = DispatchQueue(
label: "com.mobilecoin.\(Account.self).\(Self.self)",
target: targetQueue)
self.account = account
self.txOutFetcher = FogView.TxOutFetcher(
fogView: account.mapLockWithoutLocking { $0.fogView },
accountKey: account.accessWithoutLocking.accountKey,
fogViewService: fogViewService,
fogQueryScalingStrategy: fogQueryScalingStrategy,
targetQueue: targetQueue)
self.viewKeyScanner = FogViewKeyScanner(
accountKey: account.accessWithoutLocking.accountKey,
fogBlockService: fogBlockService)
self.fogKeyImageChecker = FogKeyImageChecker(
fogKeyImageService: fogKeyImageService,
targetQueue: targetQueue)
}
func updateBalance(completion: @escaping (Result<Balance, ConnectionError>) -> Void) {
logger.info("Updating balance...", logFunction: false)
checkForNewTxOuts {
guard $0.successOr(completion: completion) != nil else {
logger.warning(
"Failed to update balance: checkForNewTxOuts error: \($0)",
logFunction: false)
return
}
self.checkForSpentTxOuts {
guard $0.successOr(completion: completion) != nil else {
logger.warning(
"Failed to update balance: checkForSpentTxOuts error: \($0)",
logFunction: false)
return
}
let balance = self.account.readSync { $0.cachedBalance }
logger.info(
"Balance update successful. balance: \(redacting: balance)",
logFunction: false)
completion(.success(balance))
}
}
}
func checkForNewTxOuts(completion: @escaping (Result<(), ConnectionError>) -> Void) {
checkForNewFogViewTxOuts {
guard $0.successOr(completion: completion) != nil else { return }
self.viewKeyScanUnscannedMissedBlocks(completion: completion)
}
}
func checkForNewFogViewTxOuts(completion: @escaping (Result<(), ConnectionError>) -> Void) {
txOutFetcher.fetchTxOuts(partialResultsWithWriteLock: { newTxOuts in
logger.info(
"Found \(redacting: newTxOuts.count) new TxOuts using Fog View",
logFunction: false)
let account = self.account.accessWithoutLocking
account.addTxOuts(newTxOuts)
}, completion: completion)
}
func viewKeyScanUnscannedMissedBlocks(
completion: @escaping (Result<(), ConnectionError>) -> Void
) {
let unscannedBlockRanges = account.readSync { $0.unscannedMissedBlocksRanges }
guard !unscannedBlockRanges.isEmpty else {
logger.debug("0 unscanned missed blocks, skipping.", logFunction: false)
serialQueue.async {
completion(.success(()))
}
return
}
viewKeyScanner.viewKeyScanBlocks(blockRanges: unscannedBlockRanges) {
completion($0.map { foundTxOuts in
self.account.writeSync {
$0.addViewKeyScanResults(
scannedBlockRanges: unscannedBlockRanges,
foundTxOuts: foundTxOuts)
}
})
}
}
func checkForSpentTxOuts(completion: @escaping (Result<(), ConnectionError>) -> Void) {
let keyImageTrackers = account.mapLock { account in
account.allTxOutTrackers.filter { !$0.isSpent }.map { $0.keyImageTracker }
}
let queries = keyImageTrackers.readSync {
$0.map { ($0.keyImage, $0.nextKeyImageQueryBlockIndex) }
}
fogKeyImageChecker.checkKeyImages(keyImageQueries: queries) {
completion($0.map { statuses in
keyImageTrackers.writeSync { keyImageTrackers in
for (tracker, status) in zip(keyImageTrackers, statuses) {
tracker.spentStatus = status
}
}
})
}
}
}
}

View File

@ -0,0 +1,231 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable closure_body_length
import Foundation
extension Account {
struct TransactionEstimator {
private let serialQueue: DispatchQueue
private let account: ReadWriteDispatchLock<Account>
private let feeFetcher: BlockchainFeeFetcher
private let txOutSelector: TxOutSelector
init(
account: ReadWriteDispatchLock<Account>,
feeFetcher: BlockchainFeeFetcher,
txOutSelectionStrategy: TxOutSelectionStrategy,
targetQueue: DispatchQueue?
) {
self.serialQueue = DispatchQueue(
label: "com.mobilecoin.\(Account.self).\(Self.self))",
target: targetQueue)
self.account = account
self.feeFetcher = feeFetcher
self.txOutSelector = TxOutSelector(txOutSelectionStrategy: txOutSelectionStrategy)
}
func amountTransferable(
feeLevel: FeeLevel,
completion: @escaping (Result<UInt64, BalanceTransferEstimationFetcherError>) -> Void
) {
feeFetcher.feeStrategy(for: feeLevel) {
completion($0.mapError { .connectionError($0) }
.flatMap { feeStrategy in
let txOuts = self.account.readSync { $0.unspentTxOuts }
logger.info(
"Calculating amountTransferable. feeLevel: \(feeLevel), " +
"unspentTxOutValues: \(redacting: txOuts.map { $0.value })",
logFunction: false)
return self.txOutSelector
.amountTransferable(feeStrategy: feeStrategy, txOuts: txOuts)
.mapError {
switch $0 {
case .feeExceedsBalance(let reason):
return .feeExceedsBalance(reason)
case .balanceOverflow(let reason):
return .balanceOverflow(reason)
}
}
.map {
logger.info(
"amountTransferable: \(redacting: $0)",
logFunction: false)
return $0
}
})
}
}
func estimateTotalFee(
toSendAmount amount: UInt64,
feeLevel: FeeLevel,
completion: @escaping (Result<UInt64, TransactionEstimationFetcherError>) -> Void
) {
guard amount > 0 else {
let errorMessage = "estimateTotalFee failure: Cannot spend 0 MOB"
logger.error(errorMessage, logFunction: false)
serialQueue.async {
completion(.failure(.invalidInput(errorMessage)))
}
return
}
feeFetcher.feeStrategy(for: feeLevel) {
completion($0.mapError { .connectionError($0) }
.flatMap { feeStrategy in
let txOuts = self.account.readSync { $0.unspentTxOuts }
logger.info(
"Estimating total fee: amount: \(redacting: amount), feeLevel: " +
"\(feeLevel), unspentTxOutValues: " +
"\(redacting: txOuts.map { $0.value })",
logFunction: false)
return self.txOutSelector
.estimateTotalFee(
toSendAmount: amount,
feeStrategy: feeStrategy,
txOuts: txOuts)
.mapError { _ in
TransactionEstimationFetcherError.insufficientBalance()
}
.map {
logger.info(
"estimateTotalFee: \(redacting: $0.totalFee), " +
"requiresDefrag: \($0.requiresDefrag)",
logFunction: false)
return $0.totalFee
}
})
}
}
func requiresDefragmentation(
toSendAmount amount: UInt64,
feeLevel: FeeLevel,
completion: @escaping (Result<Bool, TransactionEstimationFetcherError>) -> Void
) {
guard amount > 0 else {
let errorMessage = "requiresDefragmentation failure: Cannot spend 0 MOB"
logger.error(errorMessage, logFunction: false)
serialQueue.async {
completion(.failure(.invalidInput(errorMessage)))
}
return
}
feeFetcher.feeStrategy(for: feeLevel) {
completion($0.mapError { .connectionError($0) }
.flatMap { feeStrategy in
let txOuts = self.account.readSync { $0.unspentTxOuts }
logger.info(
"Calculation defragmentation required: amount: \(redacting: amount), " +
"feeLevel: \(feeLevel), unspentTxOutValues: " +
"\(redacting: txOuts.map { $0.value })",
logFunction: false)
return self.txOutSelector
.estimateTotalFee(
toSendAmount: amount,
feeStrategy: feeStrategy,
txOuts: txOuts)
.mapError { _ in
TransactionEstimationFetcherError.insufficientBalance()
}
.map {
logger.info(
"requiresDefragmentation: \($0.requiresDefrag), totalFee: " +
"\(redacting: $0.totalFee)",
logFunction: false)
return $0.requiresDefrag
}
})
}
}
}
}
extension Account.TransactionEstimator {
@available(*, deprecated, message: "Use amountTransferable(feeLevel:completion:) instead")
func amountTransferable(feeLevel: FeeLevel)
-> Result<UInt64, BalanceTransferEstimationError>
{
let feeStrategy = feeLevel.defaultFeeStrategy
let txOuts = account.readSync { $0.unspentTxOuts }
logger.info(
"Calculating amountTransferable. feeLevel: \(feeLevel), unspentTxOutValues: " +
"\(redacting: txOuts.map { $0.value })",
logFunction: false)
return txOutSelector.amountTransferable(feeStrategy: feeStrategy, txOuts: txOuts)
.mapError {
switch $0 {
case .feeExceedsBalance(let reason):
return .feeExceedsBalance(reason)
case .balanceOverflow(let reason):
return .balanceOverflow(reason)
}
}
.map {
logger.info("amountTransferable: \(redacting: $0)", logFunction: false)
return $0
}
}
@available(*, deprecated, message:
"Use estimateTotalFee(toSendAmount:feeLevel:completion:) instead")
func estimateTotalFee(toSendAmount amount: UInt64, feeLevel: FeeLevel)
-> Result<UInt64, TransactionEstimationError>
{
guard amount > 0 else {
let errorMessage = "estimateTotalFee failure: Cannot spend 0 MOB"
logger.error(errorMessage, logFunction: false)
return .failure(.invalidInput(errorMessage))
}
let feeStrategy = feeLevel.defaultFeeStrategy
let txOuts = account.readSync { $0.unspentTxOuts }
logger.info(
"Estimating total fee: amount: \(redacting: amount), feeLevel: \(feeLevel), " +
"unspentTxOutValues: \(redacting: txOuts.map { $0.value })",
logFunction: false)
return txOutSelector
.estimateTotalFee(toSendAmount: amount, feeStrategy: feeStrategy, txOuts: txOuts)
.mapError { _ -> TransactionEstimationError in .insufficientBalance() }
.map {
logger.info(
"estimateTotalFee: \(redacting: $0.totalFee), requiresDefrag: " +
"\($0.requiresDefrag)",
logFunction: false)
return $0.totalFee
}
}
@available(*, deprecated, message:
"Use requiresDefragmentation(toSendAmount:feeLevel:completion:) instead")
func requiresDefragmentation(toSendAmount amount: UInt64, feeLevel: FeeLevel)
-> Result<Bool, TransactionEstimationError>
{
guard amount > 0 else {
let errorMessage = "requiresDefragmentation failure: Cannot spend 0 MOB"
logger.error(errorMessage, logFunction: false)
return .failure(.invalidInput(errorMessage))
}
let feeStrategy = feeLevel.defaultFeeStrategy
let txOuts = account.readSync { $0.unspentTxOuts }
logger.info(
"Calculation defragmentation required: amount: \(redacting: amount), feeLevel: " +
"\(feeLevel), unspentTxOutValues: \(redacting: txOuts.map { $0.value })",
logFunction: false)
return txOutSelector
.estimateTotalFee(toSendAmount: amount, feeStrategy: feeStrategy, txOuts: txOuts)
.mapError { _ -> TransactionEstimationError in .insufficientBalance() }
.map {
logger.info(
"requiresDefragmentation: \($0.requiresDefrag), totalFee: " +
"\(redacting: $0.totalFee)",
logFunction: false)
return $0.requiresDefrag
}
}
}

View File

@ -0,0 +1,222 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable closure_body_length function_body_length multiline_arguments
import Foundation
extension Account {
struct TransactionOperations {
private let serialQueue: DispatchQueue
private let account: ReadWriteDispatchLock<Account>
private let feeFetcher: BlockchainFeeFetcher
private let txOutSelector: TxOutSelector
private let transactionPreparer: TransactionPreparer
init(
account: ReadWriteDispatchLock<Account>,
fogMerkleProofService: FogMerkleProofService,
fogResolverManager: FogResolverManager,
feeFetcher: BlockchainFeeFetcher,
txOutSelectionStrategy: TxOutSelectionStrategy,
mixinSelectionStrategy: MixinSelectionStrategy,
targetQueue: DispatchQueue?
) {
self.serialQueue = DispatchQueue(
label: "com.mobilecoin.\(Account.self).\(Self.self))",
target: targetQueue)
self.account = account
self.feeFetcher = feeFetcher
self.txOutSelector = TxOutSelector(txOutSelectionStrategy: txOutSelectionStrategy)
self.transactionPreparer = TransactionPreparer(
accountKey: account.accessWithoutLocking.accountKey,
fogMerkleProofService: fogMerkleProofService,
fogResolverManager: fogResolverManager,
mixinSelectionStrategy: mixinSelectionStrategy,
targetQueue: targetQueue)
}
func prepareTransaction(
to recipient: PublicAddress,
amount: UInt64,
fee: UInt64,
completion: @escaping (
Result<(transaction: Transaction, receipt: Receipt), TransactionPreparationError>
) -> Void
) {
guard amount > 0 else {
let errorMessage = "prepareTransactionWithFee failure: Cannot spend 0 MOB"
logger.error(errorMessage, logFunction: false)
serialQueue.async {
completion(.failure(.invalidInput(errorMessage)))
}
return
}
let (unspentTxOuts, ledgerBlockCount) =
account.readSync { ($0.unspentTxOuts, $0.knowableBlockCount) }
logger.info(
"Preparing transaction with provided fee... recipient: \(redacting: recipient), " +
"amount: \(redacting: amount), fee: \(redacting: fee), unspentTxOutValues: " +
"\(redacting: unspentTxOuts.map { $0.value })",
logFunction: false)
switch txOutSelector
.selectTransactionInputs(amount: amount, fee: fee, fromTxOuts: unspentTxOuts)
.mapError({ error -> TransactionPreparationError in
switch error {
case .insufficientTxOuts:
return .insufficientBalance()
case .defragmentationRequired:
return .defragmentationRequired()
}
})
{
case .success(let txOutsToSpend):
logger.info(
"Transaction prepared with fee. txOutsToSpend: " +
"0x\(redacting: txOutsToSpend.map { $0.publicKey.hexEncodedString() })",
logFunction: false)
let tombstoneBlockIndex = ledgerBlockCount + 50
transactionPreparer.prepareTransaction(
inputs: txOutsToSpend,
recipient: recipient,
amount: amount,
fee: fee,
tombstoneBlockIndex: tombstoneBlockIndex,
completion: completion)
case .failure(let error):
logger.info("prepareTransactionWithFee failure: \(error)", logFunction: false)
serialQueue.async {
completion(.failure(error))
}
}
}
func prepareTransaction(
to recipient: PublicAddress,
amount: UInt64,
feeLevel: FeeLevel,
completion: @escaping (
Result<(transaction: Transaction, receipt: Receipt), TransactionPreparationError>
) -> Void
) {
guard amount > 0 else {
let errorMessage = "prepareTransactionWithFeeLevel failure: Cannot spend 0 MOB"
logger.error(errorMessage, logFunction: false)
serialQueue.async {
completion(.failure(.invalidInput(errorMessage)))
}
return
}
feeFetcher.feeStrategy(for: feeLevel) {
switch $0 {
case .success(let feeStrategy):
let (unspentTxOuts, ledgerBlockCount) =
self.account.readSync { ($0.unspentTxOuts, $0.knowableBlockCount) }
logger.info(
"Preparing transaction with fee level... recipient: " +
"\(redacting: recipient), amount: \(redacting: amount), feeLevel: " +
"\(feeLevel), unspentTxOutValues: " +
"\(redacting: unspentTxOuts.map { $0.value })",
logFunction: false)
switch self.txOutSelector
.selectTransactionInputs(
amount: amount,
feeStrategy: feeStrategy,
fromTxOuts: unspentTxOuts)
.mapError({ error -> TransactionPreparationError in
switch error {
case .insufficientTxOuts:
return .insufficientBalance()
case .defragmentationRequired:
return .defragmentationRequired()
}
})
{
case .success(let (inputs: inputs, fee: fee)):
logger.info(
"Transaction prepared with fee level. fee: \(redacting: fee)",
logFunction: false)
let tombstoneBlockIndex = ledgerBlockCount + 50
self.transactionPreparer.prepareTransaction(
inputs: inputs,
recipient: recipient,
amount: amount,
fee: fee,
tombstoneBlockIndex: tombstoneBlockIndex,
completion: completion)
case .failure(let error):
logger.info(
"prepareTransactionWithFeeLevel failure: \(error)",
logFunction: false)
completion(.failure(error))
}
case .failure(let connectionError):
logger.info("failure - error: \(connectionError)")
completion(.failure(.connectionError(connectionError)))
}
}
}
func prepareDefragmentationStepTransactions(
toSendAmount amountToSend: UInt64,
feeLevel: FeeLevel,
completion: @escaping (Result<[Transaction], DefragTransactionPreparationError>) -> Void
) {
guard amountToSend > 0 else {
let errorMessage =
"prepareDefragmentationStepTransactions failure: Cannot spend 0 MOB"
logger.error(errorMessage, logFunction: false)
serialQueue.async {
completion(.failure(.invalidInput(errorMessage)))
}
return
}
feeFetcher.feeStrategy(for: feeLevel) {
switch $0 {
case .success(let feeStrategy):
let (unspentTxOuts, ledgerBlockCount) =
self.account.readSync { ($0.unspentTxOuts, $0.knowableBlockCount) }
logger.info(
"Preparing defragmentation step transactions... amountToSend: " +
"\(redacting: amountToSend), feeLevel: \(feeLevel), " +
"unspentTxOutValues: \(redacting: unspentTxOuts.map { $0.value })",
logFunction: false)
switch self.txOutSelector.selectInputsForDefragTransactions(
toSendAmount: amountToSend,
feeStrategy: feeStrategy,
fromTxOuts: unspentTxOuts)
{
case .success(let defragTxInputs):
if !defragTxInputs.isEmpty {
logger.info(
"Preparing \(defragTxInputs.count) defrag transactions",
logFunction: false)
}
let tombstoneBlockIndex = ledgerBlockCount + 50
defragTxInputs.mapAsync({ defragInputs, callback in
self.transactionPreparer.prepareSelfAddressedTransaction(
inputs: defragInputs.inputs,
fee: defragInputs.fee,
tombstoneBlockIndex: tombstoneBlockIndex,
completion: callback)
}, serialQueue: self.serialQueue, completion: completion)
case .failure(let error):
logger.info(
"prepareDefragmentationStepTransactions failure: \(error)",
logFunction: false)
self.serialQueue.async {
completion(.failure(.insufficientBalance()))
}
}
case .failure(let connectionError):
logger.info("failure - error: \(connectionError)")
completion(.failure(.connectionError(connectionError)))
}
}
}
}
}

View File

@ -0,0 +1,220 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
final class Account {
let accountKey: AccountKey
let fogView = FogView()
var allTxOutTrackers: [TxOutTracker] = []
init(accountKey: AccountKeyWithFog) {
self.accountKey = accountKey.accountKey
}
var publicAddress: PublicAddress {
accountKey.publicAddress
}
var unscannedMissedBlocksRanges: [Range<UInt64>] { fogView.unscannedMissedBlocksRanges }
private var allTxOutsFoundBlockCount: UInt64 {
var allTxOutsFoundBlockCount = fogView.allRngTxOutsFoundBlockCount
for unscannedMissedBlocksRange in unscannedMissedBlocksRanges
where unscannedMissedBlocksRange.lowerBound < allTxOutsFoundBlockCount
{
allTxOutsFoundBlockCount = unscannedMissedBlocksRange.lowerBound
}
return allTxOutsFoundBlockCount
}
/// The number of blocks for which we have complete knowledge of this Account's wallet.
var knowableBlockCount: UInt64 {
var knowableBlockCount = allTxOutsFoundBlockCount
for txOut in allTxOutTrackers {
if case .unspent(let knownToBeUnspentBlockCount) = txOut.spentStatus {
knowableBlockCount = min(knowableBlockCount, knownToBeUnspentBlockCount)
}
}
return knowableBlockCount
}
var cachedBalance: Balance {
let blockCount = knowableBlockCount
let txOutValues = allTxOutTrackers
.filter { $0.receivedAndUnspent(asOfBlockCount: blockCount) }
.map { $0.knownTxOut.value }
return Balance(values: txOutValues, blockCount: blockCount)
}
var cachedAccountActivity: AccountActivity {
let blockCount = knowableBlockCount
let txOuts = allTxOutTrackers.compactMap { OwnedTxOut($0, atBlockCount: blockCount) }
return AccountActivity(txOuts: txOuts, blockCount: blockCount)
}
var ownedTxOuts: [KnownTxOut] {
ownedTxOutsAndBlockCount.txOuts
}
var ownedTxOutsAndBlockCount: (txOuts: [KnownTxOut], blockCount: UInt64) {
let knowableBlockCount = self.knowableBlockCount
let txOuts = allTxOutTrackers
.filter { $0.received(asOfBlockCount: knowableBlockCount) }
.map { $0.knownTxOut }
return (txOuts: txOuts, blockCount: knowableBlockCount)
}
var unspentTxOuts: [KnownTxOut] {
unspentTxOutsAndBlockCount.txOuts
}
var unspentTxOutsAndBlockCount: (txOuts: [KnownTxOut], blockCount: UInt64) {
let knowableBlockCount = self.knowableBlockCount
let txOuts = allTxOutTrackers
.filter { $0.receivedAndUnspent(asOfBlockCount: knowableBlockCount) }
.map { $0.knownTxOut }
return (txOuts: txOuts, blockCount: knowableBlockCount)
}
func addTxOuts(_ txOuts: [KnownTxOut]) {
allTxOutTrackers.append(contentsOf: txOuts.map { TxOutTracker($0) })
}
func addViewKeyScanResults(scannedBlockRanges: [Range<UInt64>], foundTxOuts: [KnownTxOut]) {
addTxOuts(foundTxOuts)
fogView.markBlocksAsScanned(blockRanges: scannedBlockRanges)
}
func cachedReceivedStatus(of receipt: Receipt)
-> Result<Receipt.ReceivedStatus, InvalidInputError>
{
ownedTxOut(for: receipt).map {
if let ownedTxOut = $0 {
return .received(block: ownedTxOut.block)
} else {
let knownToBeNotReceivedBlockCount = allTxOutsFoundBlockCount
guard receipt.txTombstoneBlockIndex > knownToBeNotReceivedBlockCount else {
return .tombstoneExceeded
}
return .notReceived(knownToBeNotReceivedBlockCount: knownToBeNotReceivedBlockCount)
}
}
}
/// Retrieves the `KnownTxOut`'s corresponding to `receipt` and verifies `receipt` is valid.
private func ownedTxOut(for receipt: Receipt) -> Result<KnownTxOut?, InvalidInputError> {
logger.debug(
"Last received TxOut: TxOut pubkey: " +
"\(redacting: ownedTxOuts.last?.publicKey.hexEncodedString() ?? "None")",
logFunction: false)
// First check if we've received the TxOut (either from Fog View or from view key scanning).
// This has the benefit of providing a guarantee that the TxOut is owned by this account.
guard let ownedTxOut = ownedTxOut(for: receipt.txOutPublicKeyTyped) else {
return .success(nil)
}
// Make sure the Receipt data matches the TxOut found in the ledger. This verifies that the
// public key, commitment, and masked value match.
//
// Note: This doesn't verify the confirmation number or tombstone block (since neither are
// saved to the ledger).
guard receipt.matchesTxOut(ownedTxOut) else {
let errorMessage =
"Receipt data doesn't match the corresponding TxOut found in the ledger. " +
"Receipt: \(redacting: receipt.serializedData.base64EncodedString()) - " +
"Account TxOut: \(redacting: ownedTxOut)"
logger.error(errorMessage, sensitive: true, logFunction: false)
return .failure(InvalidInputError(errorMessage))
}
// Verify that the confirmation number validates for this account key. This provides a
// guarantee that the sender of the Receipt was the creator of the TxOut that we received.
guard receipt.validateConfirmationNumber(accountKey: accountKey) else {
let errorMessage = "Receipt confirmation number is invalid for this account. " +
"Receipt: \(redacting: receipt.serializedData.base64EncodedString())"
logger.error(errorMessage, sensitive: true, logFunction: false)
return .failure(InvalidInputError(errorMessage))
}
return .success(ownedTxOut)
}
private func ownedTxOut(for txOutPublicKey: RistrettoPublic) -> KnownTxOut? {
ownedTxOuts.first(where: { $0.publicKey == txOutPublicKey })
}
}
extension Account {
/// - Returns: `.failure` if `accountKey` doesn't use Fog.
static func make(accountKey: AccountKey) -> Result<Account, InvalidInputError> {
guard let accountKey = AccountKeyWithFog(accountKey: accountKey) else {
let errorMessage = "Accounts without fog URLs are not currently supported."
logger.error(errorMessage, logFunction: false)
return .failure(InvalidInputError(errorMessage))
}
return .success(Account(accountKey: accountKey))
}
}
extension Account: CustomRedactingStringConvertible {
var redactingDescription: String {
publicAddress.redactingDescription
}
}
final class TxOutTracker {
let knownTxOut: KnownTxOut
var keyImageTracker: KeyImageSpentTracker
init(_ knownTxOut: KnownTxOut) {
self.knownTxOut = knownTxOut
self.keyImageTracker = KeyImageSpentTracker(knownTxOut.keyImage)
}
var spentStatus: KeyImage.SpentStatus {
keyImageTracker.spentStatus
}
var isSpent: Bool {
keyImageTracker.isSpent
}
func receivedAndUnspent(asOfBlockCount blockCount: UInt64) -> Bool {
received(asOfBlockCount: blockCount) && !spent(asOfBlockCount: blockCount)
}
func received(asOfBlockCount blockCount: UInt64) -> Bool {
knownTxOut.block.index < blockCount
}
func spent(asOfBlockCount blockCount: UInt64) -> Bool {
if case .spent = keyImageTracker.spentStatus.status(atBlockCount: blockCount) {
return true
}
return false
}
}
extension OwnedTxOut {
fileprivate init?(_ txOutTracker: TxOutTracker, atBlockCount blockCount: UInt64) {
guard txOutTracker.knownTxOut.block.index < blockCount else {
return nil
}
let receivedBlock = txOutTracker.knownTxOut.block
let spentBlock: BlockMetadata?
if case .spent(let block) = txOutTracker.spentStatus, block.index < blockCount {
spentBlock = block
} else {
spentBlock = nil
}
self.init(txOutTracker.knownTxOut, receivedBlock: receivedBlock, spentBlock: spentBlock)
}
}

View File

@ -0,0 +1,18 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
/// Provides a snapshot of account activity at a particular point in the ledger, as indicated by
/// `blockCount`.
public struct AccountActivity {
public let txOuts: Set<OwnedTxOut>
public let blockCount: UInt64
init(txOuts: [OwnedTxOut], blockCount: UInt64) {
self.txOuts = Set(txOuts)
self.blockCount = blockCount
}
}

View File

@ -0,0 +1,189 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
public struct AccountKey {
static func make(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
fogReportUrl: String,
fogReportId: String,
fogAuthoritySpki: Data,
subaddressIndex: UInt64 = McConstants.DEFAULT_SUBADDRESS_INDEX
) -> Result<AccountKey, InvalidInputError> {
FogInfo.make(
reportUrl: fogReportUrl,
reportId: fogReportId,
authoritySpki: fogAuthoritySpki
).map { fogInfo in
AccountKey(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
fogInfo: fogInfo,
subaddressIndex: subaddressIndex)
}
}
let viewPrivateKey: RistrettoPrivate
let spendPrivateKey: RistrettoPrivate
let fogInfo: FogInfo?
let subaddressIndex: UInt64
public let publicAddress: PublicAddress
init(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
fogInfo: FogInfo? = nil,
subaddressIndex: UInt64 = McConstants.DEFAULT_SUBADDRESS_INDEX
) {
self.viewPrivateKey = viewPrivateKey
self.spendPrivateKey = spendPrivateKey
self.fogInfo = fogInfo
self.subaddressIndex = subaddressIndex
self.publicAddress = PublicAddress(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
accountKeyFogInfo: fogInfo,
subaddressIndex: subaddressIndex)
}
/// - Returns: `nil` when the input is not deserializable.
public init?(serializedData: Data) {
guard let proto = try? External_AccountKey(serializedData: serializedData) else {
logger.error("External_AccountKey deserialization failed.", logFunction: false)
return nil
}
self.init(proto)
}
public var serializedData: Data {
let proto = External_AccountKey(self)
return proto.serializedDataInfallible
}
var fogReportUrlString: String? { fogInfo?.reportUrlString }
var fogReportUrl: FogUrl? { fogInfo?.reportUrl }
var fogReportId: String? { fogInfo?.reportId }
var fogAuthoritySpki: Data? { fogInfo?.authoritySpki }
var subaddressViewPrivateKey: RistrettoPrivate {
AccountKeyUtils.subaddressPrivateKeys(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
subaddressIndex: subaddressIndex
).subaddressViewPrivateKey
}
var subaddressSpendPrivateKey: RistrettoPrivate {
AccountKeyUtils.subaddressPrivateKeys(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
subaddressIndex: subaddressIndex
).subaddressSpendPrivateKey
}
}
extension AccountKey: Equatable {}
extension AccountKey: Hashable {}
extension AccountKey {
init?(
_ proto: External_AccountKey,
subaddressIndex: UInt64 = McConstants.DEFAULT_SUBADDRESS_INDEX
) {
guard let viewPrivateKey = RistrettoPrivate(proto.viewPrivateKey.data),
let spendPrivateKey = RistrettoPrivate(proto.spendPrivateKey.data)
else {
return nil
}
let maybeFogInfo: FogInfo?
if !proto.fogReportURL.isEmpty {
guard case .success(let fogInfo) = FogInfo.make(
reportUrl: proto.fogReportURL,
reportId: proto.fogReportID,
authoritySpki: proto.fogAuthoritySpki)
else {
return nil
}
maybeFogInfo = fogInfo
} else {
maybeFogInfo = nil
}
self.init(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
fogInfo: maybeFogInfo,
subaddressIndex: subaddressIndex)
}
}
extension External_AccountKey {
init(_ accountKey: AccountKey) {
self.init()
self.viewPrivateKey = External_RistrettoPrivate(accountKey.viewPrivateKey)
self.spendPrivateKey = External_RistrettoPrivate(accountKey.spendPrivateKey)
if let fogInfo = accountKey.fogInfo {
self.fogReportURL = fogInfo.reportUrlString
self.fogReportID = fogInfo.reportId
self.fogAuthoritySpki = fogInfo.authoritySpki
}
}
}
extension AccountKey {
struct FogInfo {
fileprivate static func make(reportUrl: String, reportId: String, authoritySpki: Data)
-> Result<FogInfo, InvalidInputError>
{
FogUrl.make(string: reportUrl).map { reportUrlTyped in
FogInfo(
reportUrlString: reportUrl,
reportUrl: reportUrlTyped,
reportId: reportId,
authoritySpki: authoritySpki)
}
}
let reportUrlString: String
let reportUrl: FogUrl
let reportId: String
let authoritySpki: Data
private init(
reportUrlString: String,
reportUrl: FogUrl,
reportId: String,
authoritySpki: Data
) {
self.reportUrlString = reportUrlString
self.reportUrl = reportUrl
self.reportId = reportId
self.authoritySpki = authoritySpki
}
}
}
extension AccountKey.FogInfo: Equatable {}
extension AccountKey.FogInfo: Hashable {}
struct AccountKeyWithFog {
let accountKey: AccountKey
let fogInfo: AccountKey.FogInfo
init?(accountKey: AccountKey) {
guard let fogInfo = accountKey.fogInfo else {
return nil
}
self.accountKey = accountKey
self.fogInfo = fogInfo
}
}

View File

@ -0,0 +1,232 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable function_parameter_count
import Foundation
import LibMobileCoin
enum AccountKeyUtils {
static func subaddressPrivateKeys(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
subaddressIndex: UInt64
) -> (subaddressViewPrivateKey: RistrettoPrivate, subaddressSpendPrivateKey: RistrettoPrivate) {
var subaddressViewPrivateKeyOut = Data32()
var subaddressSpendPrivateKeyOut = Data32()
viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
spendPrivateKey.asMcBuffer { spendKeyBufferPtr in
subaddressViewPrivateKeyOut.asMcMutableBuffer { viewPrivateKeyOutPtr in
subaddressSpendPrivateKeyOut.asMcMutableBuffer { spendPrivateKeyOutPtr in
withMcInfallible {
mc_account_key_get_subaddress_private_keys(
viewKeyBufferPtr,
spendKeyBufferPtr,
subaddressIndex,
viewPrivateKeyOutPtr,
spendPrivateKeyOutPtr)
}
}
}
}
}
// Safety: It's safe to skip validation because mc_account_key_get_subaddress_private_keys
// should always return valid RistrettoPrivate values on success.
return (RistrettoPrivate(skippingValidation: subaddressViewPrivateKeyOut),
RistrettoPrivate(skippingValidation: subaddressSpendPrivateKeyOut))
}
static func publicAddressPublicKeys(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
subaddressIndex: UInt64
) -> (viewPublicKey: RistrettoPublic, spendPublicKey: RistrettoPublic) {
var viewPublicKeyOut = Data32()
var spendPublicKeyOut = Data32()
viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
spendPrivateKey.asMcBuffer { spendKeyBufferPtr in
viewPublicKeyOut.asMcMutableBuffer { viewPublicKeyOutPtr in
spendPublicKeyOut.asMcMutableBuffer { spendPublicKeyOutPtr in
withMcInfallible {
mc_account_key_get_public_address_public_keys(
viewKeyBufferPtr,
spendKeyBufferPtr,
subaddressIndex,
viewPublicKeyOutPtr,
spendPublicKeyOutPtr)
}
}
}
}
}
// Safety: It's safe to skip validation because
// mc_account_key_get_public_address_public_keys should always return valid RistrettoPublic
// values on success.
return (RistrettoPublic(skippingValidation: viewPublicKeyOut),
RistrettoPublic(skippingValidation: spendPublicKeyOut))
}
static func fogAuthoritySig(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
reportUrl: String,
reportId: String,
authoritySpki: Data,
subaddressIndex: UInt64
) -> Data {
McAccountKey.withUnsafePointer(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
reportUrl: reportUrl,
reportId: reportId,
authoritySpki: authoritySpki
) { accountKeyPtr in
Data(withFixedLengthMcMutableBufferInfallible: McConstants.SCHNORRKEL_SIGNATURE_LEN)
{ bufferPtr in
mc_account_key_get_public_address_fog_authority_sig(
accountKeyPtr,
subaddressIndex,
bufferPtr)
}
}
}
}
extension McAccountKey {
fileprivate static func withUnsafePointer<T>(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
reportUrl: String,
reportId: String,
authoritySpki: Data,
body: (UnsafePointer<McAccountKey>) throws -> T
) rethrows -> T {
try McAccountKeyFogInfo.withUnsafePointer(
reportUrl: reportUrl,
reportId: reportId,
authoritySpki: authoritySpki
) { fogInfoPtr in
try viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
try spendPrivateKey.asMcBuffer { spendKeyBufferPtr in
var publicAddress = McAccountKey(
view_private_key: viewKeyBufferPtr,
spend_private_key: spendKeyBufferPtr,
fog_info: fogInfoPtr)
return try body(&publicAddress)
}
}
}
}
fileprivate static func withUnsafePointer<T>(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
body: (UnsafePointer<McAccountKey>) throws -> T
) rethrows -> T {
try viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
try spendPrivateKey.asMcBuffer { spendKeyBufferPtr in
var publicAddress = McAccountKey(
view_private_key: viewKeyBufferPtr,
spend_private_key: spendKeyBufferPtr,
fog_info: nil)
return try body(&publicAddress)
}
}
}
}
extension AccountKey: CStructWrapper {
typealias CStruct = McAccountKey
func withUnsafeCStructPointer<R>(
_ body: (UnsafePointer<McAccountKey>) throws -> R
) rethrows -> R {
try fogInfo.withUnsafeCStructPointer { fogInfoPtr in
try viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
try spendPrivateKey.asMcBuffer { spendKeyBufferPtr in
var publicAddress = McAccountKey(
view_private_key: viewKeyBufferPtr,
spend_private_key: spendKeyBufferPtr,
fog_info: fogInfoPtr)
return try body(&publicAddress)
}
}
}
}
}
extension McAccountKeyFogInfo {
fileprivate static func withUnsafePointer<T>(
reportUrl: String,
reportId: String,
authoritySpki: Data,
body: (UnsafePointer<McAccountKeyFogInfo>) throws -> T
) rethrows -> T {
try reportUrl.withCString { reportUrlPtr in
try reportId.withCString { reportIdPtr in
try authoritySpki.asMcBuffer { authoritySpkiPtr in
var mcFogInfo = McAccountKeyFogInfo(
report_url: reportUrlPtr,
report_id: reportIdPtr,
authority_fingerprint: authoritySpkiPtr)
return try body(&mcFogInfo)
}
}
}
}
}
extension AccountKey.FogInfo: CStructWrapper {
typealias CStruct = McAccountKeyFogInfo
func withUnsafeCStructPointer<R>(
_ body: (UnsafePointer<McAccountKeyFogInfo>) throws -> R
) rethrows -> R {
try McAccountKeyFogInfo.withUnsafePointer(
reportUrl: reportUrlString,
reportId: reportId,
authoritySpki: authoritySpki,
body: body)
}
}
extension PublicAddress: CStructWrapper {
typealias CStruct = McPublicAddress
func withUnsafeCStructPointer<R>(
_ body: (UnsafePointer<McPublicAddress>) throws -> R
) rethrows -> R {
try viewPublicKey.asMcBuffer { viewKeyBufferPtr in
try spendPublicKey.asMcBuffer { spendKeyBufferPtr in
try fogInfo.withUnsafeCStructPointer { fogInfoPtr in
var publicAddress = McPublicAddress(
view_public_key: viewKeyBufferPtr,
spend_public_key: spendKeyBufferPtr,
fog_info: fogInfoPtr)
return try body(&publicAddress)
}
}
}
}
}
extension PublicAddress.FogInfo: CStructWrapper {
typealias CStruct = McPublicAddressFogInfo
func withUnsafeCStructPointer<R>(
_ body: (UnsafePointer<McPublicAddressFogInfo>) throws -> R
) rethrows -> R {
try reportUrlString.withCString { reportUrlPtr in
try reportId.withCString { reportIdPtr in
try authoritySig.asMcBuffer { authoritySigPtr in
var mcFogInfo = McPublicAddressFogInfo(
report_url: reportUrlPtr,
report_id: reportIdPtr,
authority_sig: authoritySigPtr)
return try body(&mcFogInfo)
}
}
}
}
}

View File

@ -0,0 +1,108 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
public struct Balance {
public let amountPicoMobLow: UInt64
public let amountPicoMobHigh: UInt8
let blockCount: UInt64
init(values: [UInt64], blockCount: UInt64) {
var amountLow: UInt64 = 0
var amountHigh: UInt8 = 0
for value in values {
let (partialValue, overflow) = amountLow.addingReportingOverflow(value)
amountLow = partialValue
if overflow {
amountHigh += 1
}
}
self.init(amountLow: amountLow, amountHigh: amountHigh, blockCount: blockCount)
}
init(amountLow: UInt64, amountHigh: UInt8, blockCount: UInt64) {
self.amountPicoMobLow = amountLow
self.amountPicoMobHigh = amountHigh
self.blockCount = blockCount
}
/// - Returns: `nil` when the amount is too large to fit in a `UInt64`.
public func amountPicoMob() -> UInt64? {
guard amountPicoMobHigh == 0 else {
return nil
}
return amountPicoMobLow
}
/// Convenience accessor for balance value. `mobInt` is the integer part of the value when
/// represented in MOB. `picoFrac` is the fractional part of the value when represented in MOB.
/// However, rather than reprenting the fractional part as a decimal fraction, it is represented
/// in picoMOB, thus allowing both parts to be integer values.
///
/// The purpose of this representation is to facilitate presenting the balance to the user in
/// MOB form.
///
/// To illustrate, given an amount in the form of XXXXXXXXX.YYYYYYYYYYYY MOB,
/// - `mobInt`: XXXXXXXXX (denominated in MOB)
/// - `picoFrac`: YYYYYYYYYYYY (denominated in picoMOB)
///
/// It is necessary to break apart the values into 2 parts because the total max possible
/// balance is too large to fit in a single `UInt64`, when denominated in picoMOB, assuming 250
/// million MOB in circulation and assuming a base unit of 1 picoMOB as the smallest indivisible
/// unit of MOB.
public var amountMobParts: (mobInt: UInt32, picoFrac: UInt64) {
// amount (picoMOB) = amountLow + amountHigh * 2^64
//
// amountLowMobDec = amountLow / 10^12
// amountHighMobDec = amountHigh * 2^64 / 10^12
//
// amountMobDec = amountLowMobDec + amountHighMobDec
//
// amountLowMobInt = floor(amountLow / 10^12)
// amountLowPicoFrac = amountLow % 10^12
// amountHighMobInt = floor((amountHigh * 2^64) / 10^12)
// = floor((amountHigh << 52) / 5^12)
// amountHighPicoFrac = (amountHigh * 2^64) % 10^12
// = ((amountHigh << 52) % 5^12) << 12
//
// amountPicoFracCarry = floor((amountLowPicoFrac + amountHighPicoFrac) / 10^12)
//
// amountMobInt = amountLowMobInt + amountHighMobInt + amountPicoFracCarry
// amountPicoFrac = (amountLowPicoFrac + amountHighPicoFrac) % 10^12
let (amountLowMobInt, amountLowPicoFrac) = { () -> (UInt32, UInt64) in
// 10^12 = 1_000_000_000_000
let mobParts = amountPicoMobLow.quotientAndRemainder(dividingBy: 1_000_000_000_000)
return (UInt32(mobParts.quotient), mobParts.remainder)
}()
let (amountHighMobInt, amountHighPicoFrac) = { () -> (UInt32, UInt64) in
// Intermediary = base of 5^-12 MOB
let amountHighIntermediary = UInt64(amountPicoMobHigh) << 52
// 5^12 = 244_140_625
let mobParts = amountHighIntermediary.quotientAndRemainder(dividingBy: 244_140_625)
return (UInt32(mobParts.quotient), mobParts.remainder << 12)
}()
let amountPicoFracParts = (amountLowPicoFrac + amountHighPicoFrac).quotientAndRemainder(
dividingBy: 1_000_000_000_000)
let amountMobInt = amountLowMobInt + amountHighMobInt + UInt32(amountPicoFracParts.quotient)
let amountPicoFrac = amountPicoFracParts.remainder
return (amountMobInt, amountPicoFrac)
}
}
extension Balance: Equatable {}
extension Balance: Hashable {}
extension Balance: CustomStringConvertible {
public var description: String {
let amountMob = amountMobParts
return String(format: "%u.%012llu MOB", amountMob.mobInt, amountMob.picoFrac)
}
}

View File

@ -0,0 +1,36 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
public struct OwnedTxOut {
let publicKeyTyped: RistrettoPublic
/// - Returns: `TxOut` public key
public var publicKey: Data { publicKeyTyped.data }
public let value: UInt64
let keyImageTyped: KeyImage
/// - Returns: `TxOut` key image
public var keyImage: Data { keyImageTyped.data }
public let receivedBlock: BlockMetadata
public let spentBlock: BlockMetadata?
init(
_ knownTxOut: KnownTxOut,
receivedBlock: BlockMetadata,
spentBlock: BlockMetadata?
) {
self.publicKeyTyped = knownTxOut.publicKey
self.value = knownTxOut.value
self.keyImageTyped = knownTxOut.keyImage
self.receivedBlock = receivedBlock
self.spentBlock = spentBlock
}
}
extension OwnedTxOut: Equatable {}
extension OwnedTxOut: Hashable {}

View File

@ -0,0 +1,197 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
public struct PublicAddress {
static func make(
viewPublicKey: RistrettoPublic,
spendPublicKey: RistrettoPublic,
fogReportUrl: String,
fogReportId: String,
fogAuthoritySig: Data
) -> Result<PublicAddress, InvalidInputError> {
FogInfo.make(
reportUrl: fogReportUrl,
reportId: fogReportId,
authoritySig: fogAuthoritySig)
.map { fogInfo in
PublicAddress(
viewPublicKey: viewPublicKey,
spendPublicKey: spendPublicKey,
fogInfo: fogInfo)
}
}
let viewPublicKeyTyped: RistrettoPublic
let spendPublicKeyTyped: RistrettoPublic
let fogInfo: FogInfo?
init(viewPublicKey: RistrettoPublic, spendPublicKey: RistrettoPublic, fogInfo: FogInfo? = nil) {
self.viewPublicKeyTyped = viewPublicKey
self.spendPublicKeyTyped = spendPublicKey
self.fogInfo = fogInfo
}
/// - Returns: `nil` when the input is not deserializable.
public init?(serializedData: Data) {
guard let proto = try? External_PublicAddress(serializedData: serializedData) else {
logger.warning("External_PublicAddress deserialization failed. serializedData: " +
"\(redacting: serializedData.base64EncodedString())")
return nil
}
self.init(proto)
}
public var serializedData: Data {
let proto = External_PublicAddress(self)
return proto.serializedDataInfallible
}
/// Subaddress view public key, `C`, in bytes.
public var viewPublicKey: Data { viewPublicKeyTyped.data }
/// Subaddress spend public key, `D`, in bytes.
public var spendPublicKey: Data { spendPublicKeyTyped.data }
public var fogReportUrlString: String? { fogInfo?.reportUrlString }
var fogReportUrl: FogUrl? { fogInfo?.reportUrl }
var fogReportId: String? { fogInfo?.reportId }
var fogAuthoritySig: Data? { fogInfo?.authoritySig }
}
extension PublicAddress: Equatable {}
extension PublicAddress: Hashable {}
extension PublicAddress: CustomRedactingStringConvertible {
var redactingDescription: String {
"PublicAddress(\(Base58Coder.encode(self)))"
}
}
extension PublicAddress {
init(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
accountKeyFogInfo: AccountKey.FogInfo? = nil,
subaddressIndex: UInt64 = McConstants.DEFAULT_SUBADDRESS_INDEX
) {
let (viewPublicKey, spendPublicKey) = AccountKeyUtils.publicAddressPublicKeys(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
subaddressIndex: subaddressIndex)
let fogInfo: FogInfo?
if let accountKeyFogInfo = accountKeyFogInfo {
fogInfo = FogInfo(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
accountKeyFogInfo: accountKeyFogInfo,
subaddressIndex: subaddressIndex)
} else {
fogInfo = nil
}
self.init(viewPublicKey: viewPublicKey, spendPublicKey: spendPublicKey, fogInfo: fogInfo)
}
}
extension PublicAddress {
init?(_ publicAddress: External_PublicAddress) {
guard let viewPublicKey = RistrettoPublic(publicAddress.viewPublicKey),
let spendPublicKey = RistrettoPublic(publicAddress.spendPublicKey)
else {
return nil
}
let fogInfo: FogInfo?
if !publicAddress.fogReportURL.isEmpty {
guard case .success(let maybeFogInfo) = FogInfo.make(
reportUrl: publicAddress.fogReportURL,
reportId: publicAddress.fogReportID,
authoritySig: publicAddress.fogAuthoritySig)
else {
return nil
}
fogInfo = maybeFogInfo
} else {
fogInfo = nil
}
self.init(viewPublicKey: viewPublicKey, spendPublicKey: spendPublicKey, fogInfo: fogInfo)
}
}
extension External_PublicAddress {
init(_ publicAddress: PublicAddress) {
self.init()
self.viewPublicKey = External_CompressedRistretto(publicAddress.viewPublicKey)
self.spendPublicKey = External_CompressedRistretto(publicAddress.spendPublicKey)
if let fogInfo = publicAddress.fogInfo {
self.fogReportURL = fogInfo.reportUrlString
self.fogReportID = fogInfo.reportId
self.fogAuthoritySig = fogInfo.authoritySig
}
}
}
extension PublicAddress {
struct FogInfo {
fileprivate static func make(reportUrl: String, reportId: String, authoritySig: Data)
-> Result<FogInfo, InvalidInputError>
{
FogUrl.make(string: reportUrl).map { reportUrlTyped in
FogInfo(
reportUrlString: reportUrl,
reportUrl: reportUrlTyped,
reportId: reportId,
authoritySig: authoritySig)
}
}
let reportUrlString: String
let reportUrl: FogUrl
let reportId: String
let authoritySig: Data
private init(
reportUrlString: String,
reportUrl: FogUrl,
reportId: String,
authoritySig: Data
) {
self.reportUrlString = reportUrlString
self.reportUrl = reportUrl
self.reportId = reportId
self.authoritySig = authoritySig
}
}
}
extension PublicAddress.FogInfo: Equatable {}
extension PublicAddress.FogInfo: Hashable {}
extension PublicAddress.FogInfo {
fileprivate init(
viewPrivateKey: RistrettoPrivate,
spendPrivateKey: RistrettoPrivate,
accountKeyFogInfo: AccountKey.FogInfo,
subaddressIndex: UInt64 = McConstants.DEFAULT_SUBADDRESS_INDEX
) {
let authoritySig = AccountKeyUtils.fogAuthoritySig(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
reportUrl: accountKeyFogInfo.reportUrlString,
reportId: accountKeyFogInfo.reportId,
authoritySpki: accountKeyFogInfo.authoritySpki,
subaddressIndex: subaddressIndex)
self.init(
reportUrlString: accountKeyFogInfo.reportUrlString,
reportUrl: accountKeyFogInfo.reportUrl,
reportId: accountKeyFogInfo.reportId,
authoritySig: authoritySig)
}
}

208
Sources/Common/Errors.swift Normal file
View File

@ -0,0 +1,208 @@
// swiftlint:disable:this file_name
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
public struct InvalidInputError: Error {
let reason: String
init(_ reason: String) {
self.reason = reason
}
}
extension InvalidInputError: CustomStringConvertible {
public var description: String {
"Invalid input: \(reason)"
}
}
public enum ConnectionError: Error {
case connectionFailure(String)
case authorizationFailure(String)
case invalidServerResponse(String)
case attestationVerificationFailed(String)
case outdatedClient(String)
case serverRateLimited(String)
}
extension ConnectionError: CustomStringConvertible {
public var description: String {
"Connection error: " + {
switch self {
case .connectionFailure(let reason):
return "Connection failure: \(reason)"
case .authorizationFailure(let reason):
return "Authorization failure: \(reason)"
case .invalidServerResponse(let reason):
return "Invalid server response: \(reason)"
case .attestationVerificationFailed(let reason):
return "Attestation verification failed: \(reason)"
case .outdatedClient(let reason):
return "Outdated client: \(reason)"
case .serverRateLimited(let reason):
return "Server rate limited: \(reason)"
}
}()
}
}
public enum BalanceTransferEstimationFetcherError: Error {
case feeExceedsBalance(String = String())
case balanceOverflow(String = String())
case connectionError(ConnectionError)
}
extension BalanceTransferEstimationFetcherError: CustomStringConvertible {
public var description: String {
"Balance transfer estimation error: " + {
switch self {
case .feeExceedsBalance(let reason):
return "Fee exceeds balance\(!reason.isEmpty ? ": \(reason)" : "")"
case .balanceOverflow(let reason):
return "Balance overflow\(!reason.isEmpty ? ": \(reason)" : "")"
case .connectionError(let innerError):
return "\(innerError)"
}
}()
}
}
public enum TransactionEstimationFetcherError: Error {
case invalidInput(String)
case insufficientBalance(String = String())
case connectionError(ConnectionError)
}
extension TransactionEstimationFetcherError: CustomStringConvertible {
public var description: String {
"Transaction estimation error: " + {
switch self {
case .invalidInput(let reason):
return "Invalid input: \(reason)"
case .insufficientBalance(let reason):
return "Insufficient balance\(!reason.isEmpty ? ": \(reason)" : "")"
case .connectionError(let innerError):
return "\(innerError)"
}
}()
}
}
public enum TransactionPreparationError: Error {
case invalidInput(String)
case insufficientBalance(String = String())
case defragmentationRequired(String = String())
case connectionError(ConnectionError)
}
extension TransactionPreparationError: CustomStringConvertible {
public var description: String {
"Transaction preparation error: " + {
switch self {
case .invalidInput(let reason):
return "Invalid input: \(reason)"
case .insufficientBalance(let reason):
return "Insufficient balance\(!reason.isEmpty ? ": \(reason)" : "")"
case .defragmentationRequired(let reason):
return "Defragmentation required\(!reason.isEmpty ? ": \(reason)" : "")"
case .connectionError(let innerError):
return "\(innerError)"
}
}()
}
}
public enum DefragTransactionPreparationError: Error {
case invalidInput(String)
case insufficientBalance(String = String())
case connectionError(ConnectionError)
}
extension DefragTransactionPreparationError: CustomStringConvertible {
public var description: String {
"Defragmentation transaction preparation error: " + {
switch self {
case .invalidInput(let reason):
return "Invalid input: \(reason)"
case .insufficientBalance(let reason):
return "Insufficient balance\(!reason.isEmpty ? ": \(reason)" : "")"
case .connectionError(let innerError):
return "\(innerError)"
}
}()
}
}
public enum TransactionSubmissionError: Error {
case connectionError(ConnectionError)
case invalidTransaction(String = String())
case feeError(String = String())
case tombstoneBlockTooFar(String = String())
case missingMemo(String = String())
case inputsAlreadySpent(String = String())
}
extension TransactionSubmissionError: CustomStringConvertible {
public var description: String {
"Transaction submission error: " + {
switch self {
case .connectionError(let connectionError):
return "\(connectionError)"
case .missingMemo(let reason):
return "Missing memo error\(!reason.isEmpty ? ": \(reason)" : "")"
case .feeError(let reason):
return "Fee error\(!reason.isEmpty ? ": \(reason)" : "")"
case .invalidTransaction(let reason):
return "Invalid transaction\(!reason.isEmpty ? ": \(reason)" : "")"
case .tombstoneBlockTooFar(let reason):
return "Tombstone block too far\(!reason.isEmpty ? ": \(reason)" : "")"
case .inputsAlreadySpent(let reason):
return "Inputs already spent\(!reason.isEmpty ? ": \(reason)" : "")"
}
}()
}
}
@available(*, deprecated)
public enum BalanceTransferEstimationError: Error {
case feeExceedsBalance(String = String())
case balanceOverflow(String = String())
}
@available(*, deprecated)
extension BalanceTransferEstimationError: CustomStringConvertible {
public var description: String {
"Balance transfer estimation error: " + {
switch self {
case .feeExceedsBalance(let reason):
return "Fee exceeds balance\(!reason.isEmpty ? ": \(reason)" : "")"
case .balanceOverflow(let reason):
return "Balance overflow\(!reason.isEmpty ? ": \(reason)" : "")"
}
}()
}
}
@available(*, deprecated)
public enum TransactionEstimationError: Error {
case invalidInput(String)
case insufficientBalance(String = String())
}
@available(*, deprecated)
extension TransactionEstimationError: CustomStringConvertible {
public var description: String {
"Transaction estimation error: " + {
switch self {
case .invalidInput(let reason):
return "Invalid input: \(reason)"
case .insufficientBalance(let reason):
return "Insufficient balance\(!reason.isEmpty ? ": \(reason)" : "")"
}
}()
}
}

View File

@ -0,0 +1,28 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
enum CryptoUtils {
static func ristrettoPrivateValidate(_ bytes: Data) -> Bool {
bytes.asMcBuffer { bytesPtr in
var valid = false
withMcInfallible {
mc_ristretto_private_validate(bytesPtr, &valid)
}
return valid
}
}
static func ristrettoPublicValidate(_ bytes: Data) -> Bool {
bytes.asMcBuffer { bytesPtr in
var valid = false
withMcInfallible {
mc_ristretto_public_validate(bytesPtr, &valid)
}
return valid
}
}
}

View File

@ -0,0 +1,47 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct RistrettoPrivate {
let data32: Data32
init?(_ data: Data32) {
guard CryptoUtils.ristrettoPrivateValidate(data.data) else {
return nil
}
self.data32 = data
}
init(skippingValidation data: Data32) {
self.data32 = data
}
}
extension RistrettoPrivate: DataConvertibleImpl {
typealias Iterator = Data.Iterator
init?(_ data: Data) {
guard let data32 = Data32(data.data) else {
return nil
}
self.init(data32)
}
var data: Data { data32.data }
}
extension RistrettoPrivate {
init?(_ ristrettoPrivate: External_RistrettoPrivate) {
self.init(ristrettoPrivate.data)
}
}
extension External_RistrettoPrivate {
init(_ ristrettoPrivate: RistrettoPrivate) {
self.init()
self.data = ristrettoPrivate.data
}
}

View File

@ -0,0 +1,47 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct RistrettoPublic {
let data32: Data32
init?(_ data: Data32) {
guard CryptoUtils.ristrettoPublicValidate(data.data) else {
return nil
}
self.data32 = data
}
init(skippingValidation data: Data32) {
self.data32 = data
}
}
extension RistrettoPublic: DataConvertibleImpl {
typealias Iterator = Data.Iterator
init?(_ data: Data) {
guard let data32 = Data32(data.data) else {
return nil
}
self.init(data32)
}
var data: Data { data32.data }
}
extension RistrettoPublic {
init?(_ ristrettoPublic: External_CompressedRistretto) {
self.init(ristrettoPublic.data)
}
}
extension External_CompressedRistretto {
init(_ ristrettoPublic: RistrettoPublic) {
self.init()
self.data = ristrettoPublic.data
}
}

View File

@ -0,0 +1,88 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
enum VersionedCryptoBoxError: Error {
case invalidInput(String)
case unsupportedVersion(String)
}
extension VersionedCryptoBoxError: CustomStringConvertible {
var description: String {
"Versioned CryptoBox error: " + {
switch self {
case .invalidInput(let reason):
return "Invalid input: \(reason)"
case .unsupportedVersion(let reason):
return "Unsupported version: \(reason)"
}
}()
}
}
enum VersionedCryptoBox {
static func encrypt(
plaintext: Data,
publicKey: RistrettoPublic,
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)?,
rngContext: Any?
) -> Result<Data, InvalidInputError> {
publicKey.asMcBuffer { viewPublicKeyPtr in
plaintext.asMcBuffer { plaintextPtr in
withMcRngCallback(rng: rng, rngContext: rngContext) { rngCallbackPtr in
Data.make(withMcMutableBuffer: { bufferPtr, errorPtr in
mc_versioned_crypto_box_encrypt(
viewPublicKeyPtr,
plaintextPtr,
rngCallbackPtr,
bufferPtr,
&errorPtr)
}).mapError {
switch $0.errorCode {
case .aead:
return InvalidInputError("\(redacting: $0.description)")
default:
// Safety: mc_versioned_crypto_box_encrypt should not throw
// non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}
}
}
}
static func decrypt(
ciphertext: Data,
privateKey: RistrettoPrivate
) -> Result<Data, VersionedCryptoBoxError> {
privateKey.asMcBuffer { privateKeyPtr in
ciphertext.asMcBuffer { ciphertextPtr in
Data.make(withEstimatedLengthMcMutableBuffer: ciphertext.count)
{ bufferPtr, errorPtr in
mc_versioned_crypto_box_decrypt(
privateKeyPtr,
ciphertextPtr,
bufferPtr,
&errorPtr)
}.mapError {
switch $0.errorCode {
case .aead, .invalidInput:
return .invalidInput("\(redacting: $0.description)")
case .unsupportedCryptoBoxVersion:
return .unsupportedVersion("\(redacting: $0.description)")
default:
// Safety: mc_versioned_crypto_box_decrypt should not throw non-documented
// errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}
}
}
}

View File

@ -0,0 +1,59 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
public enum Base58DecodingResult {
case publicAddress(PublicAddress)
case paymentRequest(PaymentRequest)
case transferPayload(TransferPayload)
}
public enum Base58Coder {
public static func encode(_ publicAddress: PublicAddress) -> String {
var wrapper = Printable_PrintableWrapper()
wrapper.publicAddress = External_PublicAddress(publicAddress)
return wrapper.base58EncodedString()
}
public static func encode(_ paymentRequest: PaymentRequest) -> String {
var wrapper = Printable_PrintableWrapper()
wrapper.paymentRequest = Printable_PaymentRequest(paymentRequest)
return wrapper.base58EncodedString()
}
public static func encode(_ transferPayload: TransferPayload) -> String {
var wrapper = Printable_PrintableWrapper()
wrapper.transferPayload = Printable_TransferPayload(transferPayload)
return wrapper.base58EncodedString()
}
/// - Returns: `nil` when the input is not decodable.
public static func decode(_ base58String: String) -> Base58DecodingResult? {
guard let wrapper = Printable_PrintableWrapper(base58Encoded: base58String) else {
return nil
}
switch wrapper.wrapper {
case .publicAddress(let publicAddress):
guard let publicAddress = PublicAddress(publicAddress) else {
return nil
}
return .publicAddress(publicAddress)
case .paymentRequest(let paymentRequest):
guard let paymentRequest = PaymentRequest(paymentRequest) else {
return nil
}
return .paymentRequest(paymentRequest)
case .transferPayload(let transferPayload):
guard let transferPayload = TransferPayload(transferPayload) else {
return nil
}
return .transferPayload(transferPayload)
case .none:
return nil
}
}
}

View File

@ -0,0 +1,142 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
public enum MobUri {
public enum Payload {
case publicAddress(PublicAddress)
case paymentRequest(PaymentRequest)
case transferPayload(TransferPayload)
}
public static func decode(uri uriString: String) -> Result<Payload, InvalidInputError> {
guard let uri = URL(string: uriString) else {
logger.info("Could not parse MobURI as URL: \(redacting: uriString)")
return .failure(
InvalidInputError("Could not parse MobUri as URL: \(redacting: uriString)"))
}
guard let scheme = uri.scheme else {
logger.info("MobUri scheme cannot be empty.")
return .failure(InvalidInputError("MobUri scheme cannot be empty."))
}
guard scheme == McConstants.MOB_URI_SCHEME else {
logger.info(
"MobUri scheme must be \"\(McConstants.MOB_URI_SCHEME)\". Found: \(scheme)")
return .failure(InvalidInputError(
"MobUri scheme must be \"\(McConstants.MOB_URI_SCHEME)\". Found: \(scheme)"))
}
return Payload.make(pathComponents: uri.pathComponents)
}
public static func encode(_ publicAddress: PublicAddress) -> String {
encode(.publicAddress(publicAddress))
}
public static func encode(_ paymentRequest: PaymentRequest) -> String {
encode(.paymentRequest(paymentRequest))
}
public static func encode(_ transferPayload: TransferPayload) -> String {
encode(.transferPayload(transferPayload))
}
static func encode(_ payload: Payload) -> String {
"\(McConstants.MOB_URI_SCHEME)://\(payload.uriPath)"
}
}
extension MobUri.Payload {
static func make(pathComponents: [String]) -> Result<MobUri.Payload, InvalidInputError> {
// Foundation.URL returns "/" as the first value in pathComponents, so we normalize by
// removing it.
guard let firstComponent = pathComponents.first else {
return .failure(InvalidInputError("MobUri must have a path."))
}
guard firstComponent == "/" else {
return .failure(InvalidInputError("MobUri must have an absolute path."))
}
let pathComponents = Array(pathComponents.dropFirst())
guard pathComponents.count >= 2 else {
return .failure(InvalidInputError("MobUri must have at least 2 path components."))
}
let payloadTypeString = pathComponents[0]
guard let payloadType = PayloadType(payloadTypeString) else {
return .failure(InvalidInputError(
"MobUri contains unrecognized payload type: \(payloadTypeString)"))
}
switch payloadType {
case .b58:
let payloadString = pathComponents[1]
guard let decodingResult = Base58Coder.decode(payloadString) else {
return .failure(InvalidInputError(
"MobUri payload base-58 decoding failed. Payload: \(redacting: payloadString)"))
}
return .success(MobUri.Payload(decodingResult))
}
}
init(_ base58DecodingResult: Base58DecodingResult) {
switch base58DecodingResult {
case .publicAddress(let publicAddress):
self = .publicAddress(publicAddress)
case .paymentRequest(let paymentRequest):
self = .paymentRequest(paymentRequest)
case .transferPayload(let transferPayload):
self = .transferPayload(transferPayload)
}
}
var payloadType: PayloadType {
switch self {
case .publicAddress, .paymentRequest, .transferPayload:
return .b58
}
}
var payloadString: String {
switch self {
case .publicAddress(let publicAddress):
return Base58Coder.encode(publicAddress)
case .paymentRequest(let paymentRequest):
return Base58Coder.encode(paymentRequest)
case .transferPayload(let transferPayload):
return Base58Coder.encode(transferPayload)
}
}
var uriPath: String {
"/\(payloadType)/\(payloadString)"
}
}
extension MobUri.Payload {
enum PayloadType {
case b58
init?(_ string: String) {
switch string {
case "b58":
self = .b58
default:
return nil
}
}
}
}
extension MobUri.Payload.PayloadType: CustomStringConvertible {
var description: String {
switch self {
case .b58:
return "b58"
}
}
}

View File

@ -0,0 +1,59 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
public struct PaymentRequest {
public let publicAddress: PublicAddress
public let value: UInt64?
public let memo: String?
/// # Notes:
/// * Providing a `value` of `0` is the same as passing `nil`, meaning no value is specified.
/// * Providing an empty string for `memo` is the same as passing `nil`, meaning no memo is
/// specified.
public init(publicAddress: PublicAddress, value: UInt64? = nil, memo: String? = nil) {
self.publicAddress = publicAddress
if let value = value, value != 0 {
self.value = value
} else {
self.value = nil
}
if let memo = memo, !memo.isEmpty {
self.memo = memo
} else {
self.memo = nil
}
}
}
extension PaymentRequest: Equatable {}
extension PaymentRequest: Hashable {}
extension PaymentRequest {
init?(_ paymentRequest: Printable_PaymentRequest) {
guard let publicAddress = PublicAddress(paymentRequest.publicAddress) else {
return nil
}
self.publicAddress = publicAddress
self.value = paymentRequest.value != 0 ? paymentRequest.value : nil
self.memo = !paymentRequest.memo.isEmpty ? paymentRequest.memo : nil
}
}
extension Printable_PaymentRequest {
init(_ paymentRequest: PaymentRequest) {
self.init()
self.publicAddress = External_PublicAddress(paymentRequest.publicAddress)
if let value = paymentRequest.value {
self.value = value
}
if let memo = paymentRequest.memo {
self.memo = memo
}
}
}

View File

@ -0,0 +1,37 @@
// swiftlint:disable:this file_name
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
extension Printable_PrintableWrapper {
init?(base58Encoded base58String: String) {
guard case .success(let decodedData) =
Data.make(withMcMutableBuffer: { bufferPtr, errorPtr in
mc_printable_wrapper_b58_decode(base58String, bufferPtr, &errorPtr)
})
else {
logger.warning("PrintableWrapper base-58 decoding failed.")
return nil
}
guard let printableWrapper = try? Self(serializedData: decodedData) else {
logger.warning("Printable_PrintableWrapper deserialization failed.")
return nil
}
self = printableWrapper
}
func base58EncodedString() -> String {
let serialized = serializedDataInfallible
return serialized.asMcBuffer { bufferPtr in
String(mcString: withMcInfallibleReturningOptional {
mc_printable_wrapper_b58_encode(bufferPtr)
})
}
}
}

View File

@ -0,0 +1,49 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
public struct TransferPayload {
let rootEntropy32: Data32
let txOutPublicKey: RistrettoPublic
public let memo: String?
init(rootEntropy: Data32, txOutPublicKey: RistrettoPublic, memo: String? = nil) {
self.rootEntropy32 = rootEntropy
self.txOutPublicKey = txOutPublicKey
self.memo = memo?.isEmpty == false ? memo : nil
}
public var rootEntropy: Data {
rootEntropy32.data
}
}
extension TransferPayload: Equatable {}
extension TransferPayload: Hashable {}
extension TransferPayload {
init?(_ transferPayload: Printable_TransferPayload) {
guard let rootEntropy = Data32(transferPayload.rootEntropy),
let txOutPublicKey = RistrettoPublic(transferPayload.txOutPublicKey.data)
else {
return nil
}
self.rootEntropy32 = rootEntropy
self.txOutPublicKey = txOutPublicKey
self.memo = !transferPayload.memo.isEmpty ? transferPayload.memo : nil
}
}
extension Printable_TransferPayload {
init(_ transferPayload: TransferPayload) {
self.init()
self.rootEntropy = transferPayload.rootEntropy
self.txOutPublicKey = External_CompressedRistretto(transferPayload.txOutPublicKey)
if let memo = transferPayload.memo {
self.memo = memo
}
}
}

View File

@ -0,0 +1,113 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_arguments multiline_function_chains
import Foundation
import LibMobileCoin
struct FogKeyImageChecker {
private let serialQueue: DispatchQueue
private let fogKeyImageService: FogKeyImageService
init(fogKeyImageService: FogKeyImageService, targetQueue: DispatchQueue?) {
self.serialQueue = DispatchQueue(label: "com.mobilecoin.\(Self.self)", target: targetQueue)
self.fogKeyImageService = fogKeyImageService
}
func checkKeyImage(
keyImage: KeyImage,
nextKeyImageQueryBlockIndex: UInt64 = 0,
completion: @escaping (Result<KeyImage.SpentStatus, ConnectionError>) -> Void
) {
checkKeyImages(
keyImageQueries: [(keyImage, nextKeyImageQueryBlockIndex: nextKeyImageQueryBlockIndex)]
) {
completion($0.flatMap { statuses in
guard let keyImageStatus = statuses.first else {
return .failure(.invalidServerResponse(
"CheckKeyImage failed to return results: \(statuses)"))
}
return .success(keyImageStatus)
})
}
}
func checkKeyImages(
keyImageQueries: [KeyImage],
maxKeyImagesPerQuery: Int,
completion: @escaping (Result<[KeyImage.SpentStatus], ConnectionError>) -> Void
) {
checkKeyImages(
keyImageQueries: keyImageQueries.map { ($0, nextKeyImageQueryBlockIndex: 0) },
maxKeyImagesPerQuery: maxKeyImagesPerQuery,
completion: completion)
}
func checkKeyImages(
keyImageQueries: [(KeyImage, nextKeyImageQueryBlockIndex: UInt64)],
maxKeyImagesPerQuery: Int,
completion: @escaping (Result<[KeyImage.SpentStatus], ConnectionError>) -> Void
) {
let queryArrays = keyImageQueries.chunked(maxLength: maxKeyImagesPerQuery).map { Array($0) }
queryArrays.mapAsync({ chunk, callback in
checkKeyImages(keyImageQueries: chunk, completion: callback)
}, serialQueue: serialQueue, completion: { result in
completion(result.map { $0.flatMap { $0 } })
})
}
func checkKeyImages(
keyImageQueries: [KeyImage],
completion: @escaping (Result<[KeyImage.SpentStatus], ConnectionError>) -> Void
) {
checkKeyImages(
keyImageQueries: keyImageQueries.map { ($0, nextKeyImageQueryBlockIndex: 0) },
completion: completion)
}
func checkKeyImages(
keyImageQueries: [(KeyImage, nextKeyImageQueryBlockIndex: UInt64)],
completion: @escaping (Result<[KeyImage.SpentStatus], ConnectionError>) -> Void
) {
var request = FogLedger_CheckKeyImagesRequest()
request.queries = keyImageQueries.map {
var query = FogLedger_KeyImageQuery()
query.keyImage = External_KeyImage($0.0)
query.startBlock = $0.nextKeyImageQueryBlockIndex
return query
}
fogKeyImageService.checkKeyImages(request: request) {
completion($0.flatMap {
Self.parseResponse(keyImageQueries: keyImageQueries, response: $0)
})
}
}
private static func parseResponse(
keyImageQueries: [(KeyImage, nextKeyImageQueryBlockIndex: UInt64)],
response: FogLedger_CheckKeyImagesResponse
) -> Result<[KeyImage.SpentStatus], ConnectionError> {
keyImageQueries.map { query in
guard let keyImageResult = response.results.first(
where: { KeyImage($0.keyImage) == query.0 }) else
{
return .success(.unspent(knownToBeUnspentBlockCount: response.numBlocks))
}
switch keyImageResult.keyImageResultCodeEnum {
case .spent:
let spentAtBlock = BlockMetadata(
index: keyImageResult.spentAt,
timestampStatus: keyImageResult.timestampStatus)
return .success(.spent(block: spentAtBlock))
case .notSpent:
return .success(.unspent(knownToBeUnspentBlockCount: response.numBlocks))
case .keyImageError, .unused, .UNRECOGNIZED:
return .failure(.invalidServerResponse("Fog KeyImage result error: " +
"\(keyImageResult.keyImageResultCodeEnum), response: \(response)"))
}
}.collectResult()
}
}

View File

@ -0,0 +1,144 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_arguments multiline_function_chains
// swiftlint:disable closure_body_length
import Foundation
import LibMobileCoin
enum FogMerkleProofFetcherError: Error {
case connectionError(ConnectionError)
case outOfBounds(blockCount: UInt64, ledgerTxOutCount: UInt64)
}
extension FogMerkleProofFetcherError: CustomStringConvertible {
var description: String {
"Fog Merkle Proof Fetcher error: " + {
switch self {
case .connectionError(let innerError):
return "\(innerError)"
case let .outOfBounds(blockCount: blockCount, ledgerTxOutCount: txOutCount):
return "Out of bounds: blockCount: \(blockCount), globalTxOutCount: \(txOutCount)"
}
}()
}
}
struct FogMerkleProofFetcher {
private let serialQueue: DispatchQueue
private let fogMerkleProofService: FogMerkleProofService
init(fogMerkleProofService: FogMerkleProofService, targetQueue: DispatchQueue?) {
self.serialQueue = DispatchQueue(label: "com.mobilecoin.\(Self.self)", target: targetQueue)
self.fogMerkleProofService = fogMerkleProofService
}
func getOutputs(
globalIndicesArray: [[UInt64]],
merkleRootBlock: UInt64,
maxNumIndicesPerQuery: Int,
completion: @escaping (
Result<[[(TxOut, TxOutMembershipProof)]], FogMerkleProofFetcherError>
) -> Void
) {
getOutputs(
globalIndices: globalIndicesArray.flatMap { $0 },
merkleRootBlock: merkleRootBlock,
maxNumIndicesPerQuery: maxNumIndicesPerQuery
) {
completion($0.flatMap { allResults in
globalIndicesArray.map { globalIndices in
guard let results = allResults[globalIndices] else {
return .failure(.connectionError(.invalidServerResponse(
"Global txout indices not found in GetOutputs reponse. " +
"globalTxOutIndices: \(globalIndices), returned outputs: " +
"\(allResults)")))
}
return .success(results)
}.collectResult()
})
}
}
func getOutputs(
globalIndices: [UInt64],
merkleRootBlock: UInt64,
maxNumIndicesPerQuery: Int,
completion: @escaping (
Result<[UInt64: (TxOut, TxOutMembershipProof)], FogMerkleProofFetcherError>
) -> Void
) {
let globalIndicesArrays =
globalIndices.chunked(maxLength: maxNumIndicesPerQuery).map { Array($0) }
globalIndicesArrays.mapAsync({ chunk, callback in
getOutputs(globalIndices: chunk, merkleRootBlock: merkleRootBlock, completion: callback)
}, serialQueue: serialQueue, completion: {
completion($0.map { arrayOfOutputMaps in
arrayOfOutputMaps.reduce(into: [:]) { outputMapAccum, outputMap in
outputMapAccum.merge(outputMap, uniquingKeysWith: { key1, _ in key1 })
}
})
})
}
func getOutputs(
globalIndices: [UInt64],
merkleRootBlock: UInt64,
completion: @escaping (
Result<[UInt64: (TxOut, TxOutMembershipProof)], FogMerkleProofFetcherError>
) -> Void
) {
var request = FogLedger_GetOutputsRequest()
request.indices = globalIndices
request.merkleRootBlock = merkleRootBlock
fogMerkleProofService.getOutputs(request: request) {
completion(
$0.mapError { .connectionError($0) }
.flatMap { Self.parseResponse(response: $0) })
}
}
private static func parseResponse(response: FogLedger_GetOutputsResponse)
-> Result<[UInt64: (TxOut, TxOutMembershipProof)], FogMerkleProofFetcherError>
{
response.results.map { outputResult in
switch outputResult.resultCodeEnum {
case .exists:
break
case .doesNotExist:
return .failure(.outOfBounds(
blockCount: response.numBlocks,
ledgerTxOutCount: response.globalTxoCount))
case .outputDatabaseError, .intentionallyUnused, .UNRECOGNIZED:
return .failure(.connectionError(.invalidServerResponse(
"FogMerkleProofService.getOutputs result code error: " +
"\(outputResult.resultCodeEnum), response: \(redacting: response)")))
}
let txOut: TxOut
switch TxOut.make(outputResult.output) {
case .success(let result):
txOut = result
case .failure(let error):
return .failure(.connectionError(.invalidServerResponse(
"FogMerkleProofService.getOutputs returned invalid TxOut. error: \(error)")))
}
let membershipProof: TxOutMembershipProof
switch TxOutMembershipProof.make(outputResult.proof) {
case .success(let result):
membershipProof = result
case .failure(let error):
return .failure(.connectionError(.invalidServerResponse(
"FogMerkleProofService.getOutputs returned invalid membership proof. error: " +
"\(error)")))
}
return .success((outputResult.index, (txOut, membershipProof)))
}.collectResult().map {
Dictionary($0, uniquingKeysWith: { key1, _ in key1 })
}
}
}

View File

@ -0,0 +1,67 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct FogUntrustedTxOutFetcher {
private let fogUntrustedTxOutService: FogUntrustedTxOutService
init(fogUntrustedTxOutService: FogUntrustedTxOutService) {
self.fogUntrustedTxOutService = fogUntrustedTxOutService
}
func getTxOut(
outputPublicKey: RistrettoPublic,
completion: @escaping (
Result<(result: FogLedger_TxOutResult, blockCount: UInt64), ConnectionError>
) -> Void
) {
getTxOuts(outputPublicKeys: [outputPublicKey]) {
completion($0.flatMap { results, blockCount in
guard let result =
results.first(where: { $0.txOutPubkey.data == outputPublicKey.data })
else {
logger.info("failure - Fog UntrustedTxOut service failed to " +
"return the requested TxOut: \(redacting: results)")
return .failure(.invalidServerResponse(
"Fog UntrustedTxOut service failed to return the requested TxOut. " +
"\(results)"))
}
return .success((result, blockCount: blockCount))
})
}
}
func getTxOuts(
outputPublicKeys: [RistrettoPublic],
completion:
@escaping (
Result<(results: [FogLedger_TxOutResult], blockCount: UInt64), ConnectionError>
) -> Void
) {
logger.info(
"outputPublicKeys: \(redacting: outputPublicKeys.map { $0.hexEncodedString() })")
var request = FogLedger_TxOutRequest()
request.txOutPubkeys = outputPublicKeys.map { External_CompressedRistretto($0) }
fogUntrustedTxOutService.getTxOuts(request: request) {
completion($0.flatMap { response in
let resultPairs = response.results.map { ($0.txOutPubkey.data, $0) }
let publicKeyToResult =
Dictionary(resultPairs, uniquingKeysWith: { key1, _ in key1 })
let outputPublicKeys = outputPublicKeys.map { $0.data }
guard let results = publicKeyToResult[outputPublicKeys] else {
logger.info("failure - Fog UntrustedTxOut service failed to " +
"return the requested TxOuts: \(redacting: response.results)")
return .failure(.invalidServerResponse(
"Fog UntrustedTxOut service failed to return the requested TxOuts. " +
"\(response)"))
}
return .success((results, blockCount: response.numBlocks))
})
}
}
}

View File

@ -0,0 +1,74 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
final class FogViewKeyScanner {
private let accountKey: AccountKey
private let fogBlockService: FogBlockService
init(accountKey: AccountKey, fogBlockService: FogBlockService) {
self.accountKey = accountKey
self.fogBlockService = fogBlockService
}
func viewKeyScanBlocks(
blockRanges: [Range<UInt64>],
completion: @escaping (Result<[KnownTxOut], ConnectionError>) -> Void
) {
logger.info(
"Fetching block ranges: \(blockRanges.map { "[\($0.lowerBound), \($0.upperBound))" })",
logFunction: false)
fetchBlocksTxOuts(ranges: blockRanges) {
completion($0.map { blocksTxOuts in
logger.info(
"View key scanning blocks: " +
"\(blockRanges.map { "[\($0.lowerBound), \($0.upperBound))" }) " +
"containing \(blocksTxOuts.count) TxOuts",
logFunction: false)
let foundTxOuts = blocksTxOuts.compactMap {
$0.decrypt(accountKey: self.accountKey)
}
logger.info(
"View key scanning missed blocks found \(redacting: foundTxOuts.count) TxOuts",
logFunction: false)
return foundTxOuts
})
}
}
func fetchBlocksTxOuts(
ranges: [Range<UInt64>],
completion: @escaping (Result<[LedgerTxOut], ConnectionError>) -> Void
) {
var request = FogLedger_BlockRequest()
request.rangeValues = ranges
fogBlockService.getBlocks(request: request) {
completion($0.flatMap { response in
response.blocks.flatMap { responseBlock -> [Result<LedgerTxOut, ConnectionError>] in
let globalIndexStart =
responseBlock.globalTxoCount - UInt64(responseBlock.outputs.count)
return responseBlock.outputs.enumerated().map { outputIndex, output in
guard let partialTxOut = PartialTxOut(output) else {
let errorMessage =
"Fog Block service returned invalid TxOut: \(output)"
logger.error(errorMessage, logFunction: false)
return .failure(.invalidServerResponse(errorMessage))
}
return .success(LedgerTxOut(
partialTxOut,
globalIndex: globalIndexStart + UInt64(outputIndex),
block: responseBlock.metadata))
}
}.collectResult()
})
}
}
}

View File

@ -0,0 +1,72 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
final class FogReportManager {
private let inner: SerialDispatchLock<Inner>
private let serialQueue: DispatchQueue
private let serviceProvider: ServiceProvider
init(serviceProvider: ServiceProvider, targetQueue: DispatchQueue?) {
self.inner = .init(Inner(targetQueue: targetQueue), targetQueue: targetQueue)
self.serialQueue = DispatchQueue(label: "com.mobilecoin.\(Self.self)", target: targetQueue)
self.serviceProvider = serviceProvider
}
func reportResponse(
for reportUrl: FogUrl,
completion: @escaping (Result<Report_ReportResponse, ConnectionError>) -> Void
) {
logger.info("reportUrl: \(reportUrl.url)")
serviceProvider.fogReportService(for: reportUrl) { reportService in
self.inner.accessAsync {
let reportServer = $0.reportServer(for: reportUrl)
reportServer.reports(reportService: reportService, completion: completion)
}
}
}
func reportResponse(
for reportUrl: FogUrl,
reportParams: [(reportId: String, desiredMinPubkeyExpiry: UInt64)],
completion: @escaping (Result<Report_ReportResponse, ConnectionError>) -> Void
) {
logger.info("reportUrl: \(reportUrl.url), reportParams: \(reportParams)")
serviceProvider.fogReportService(for: reportUrl) { reportService in
self.inner.accessAsync {
let reportServer = $0.reportServer(for: reportUrl)
reportServer.reports(
reportService: reportService,
reportParams: reportParams,
completion: completion)
}
}
}
}
extension FogReportManager {
private struct Inner {
private let sharedSerialExclusionQueue: DispatchQueue
private var networkConfigToServer: [GrpcChannelConfig: FogReportServer] = [:]
init(targetQueue: DispatchQueue?) {
self.sharedSerialExclusionQueue = DispatchQueue(
label: "com.mobilecoin.\(FogReportServer.self)",
target: targetQueue)
}
mutating func reportServer(for reportUrl: FogUrl) -> FogReportServer {
let config = GrpcChannelConfig(url: reportUrl)
return networkConfigToServer[config] ?? {
let reportServer = FogReportServer(serialExclusionQueue: sharedSerialExclusionQueue)
networkConfigToServer[config] = reportServer
return reportServer
}()
}
}
}

View File

@ -0,0 +1,137 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable array_init
import Foundation
import LibMobileCoin
final class FogReportServer {
private let inner: SerialDispatchLock<Inner>
private let serialConnectionQueue: SerialCallbackQueue
init(serialExclusionQueue: DispatchQueue) {
self.inner = .init(Inner(), serialExclusionQueue: serialExclusionQueue)
self.serialConnectionQueue = .init(targetQueue: serialExclusionQueue)
}
func reports(
reportService: FogReportService,
completion: @escaping (Result<Report_ReportResponse, ConnectionError>) -> Void
) {
fetchReports(reportService: reportService, completion: completion)
}
func reports(
reportService: FogReportService,
reportParams: [(reportId: String, desiredMinPubkeyExpiry: UInt64)],
completion: @escaping (Result<Report_ReportResponse, ConnectionError>) -> Void
) {
logger.info("reportParams: \(reportParams)")
inner.accessAsync {
if let reportResponse =
$0.cachedReportResponse(satisfyingReportParams: reportParams.map { $0 })
{
completion(.success(reportResponse))
} else {
self.fetchReports(
reportService: reportService,
reportParams: reportParams,
completion: completion)
}
}
}
private func fetchReports(
reportService: FogReportService,
completion: @escaping (Result<Report_ReportResponse, ConnectionError>) -> Void
) {
serialConnectionQueue.append({ callback in
self.doFetchReports(reportService: reportService, completion: callback)
}, completion: completion)
}
private func fetchReports(
reportService: FogReportService,
reportParams: [(reportId: String, desiredMinPubkeyExpiry: UInt64)],
completion: @escaping (Result<Report_ReportResponse, ConnectionError>) -> Void
) {
logger.info("reportParams: \(reportParams)")
serialConnectionQueue.append({ callback in
// Now that we have the serialConnectionQueue lock, check again if there's a cached
// report response that satisfies the reportParams.
self.inner.accessAsync {
if let reportResponse =
$0.cachedReportResponse(satisfyingReportParams: reportParams.map { $0 })
{
callback(.success(reportResponse))
} else {
// Otherwise, continue with fetching from the network.
self.doFetchReports(reportService: reportService, completion: callback)
}
}
}, completion: completion)
}
private func doFetchReports(
reportService: FogReportService,
completion: @escaping (Result<Report_ReportResponse, ConnectionError>) -> Void
) {
reportService.getReports(request: Report_ReportRequest()) {
guard let reportResponse = $0.successOr(completion: completion) else { return }
// Save report response before releasing the serialConnectionQueue
// lock. This ensures that, if there's another request waiting, it
// will have access to the report response we just fetched.
self.cacheReportResponse(reportResponse) {
completion(.success(reportResponse))
}
}
}
private func cacheReportResponse(
_ reportResponse: Report_ReportResponse,
completion: @escaping () -> Void
) {
inner.accessAsync {
$0.cacheReportResponse(reportResponse)
completion()
}
}
}
extension FogReportServer {
private struct Inner {
private var cachedReportResponse: Report_ReportResponse?
func cachedReportResponse(
satisfyingReportParams reportParams: [(reportId: String, minPubkeyExpiry: UInt64)]
) -> Report_ReportResponse? {
logger.info("reportParams: \(reportParams)")
guard let reportResponse = cachedReportResponse else {
return nil
}
guard reportResponse.isValid(reportParams: reportParams) else {
logger.info("report response invalid - reportParams: \(reportParams)")
return nil
}
logger.info("report response valid - reportParams: \(reportParams)")
return reportResponse
}
mutating func cacheReportResponse(_ reportResponse: Report_ReportResponse) {
cachedReportResponse = reportResponse
}
}
}
extension Report_ReportResponse {
fileprivate func isValid(reportParams: [(reportId: String, minPubkeyExpiry: UInt64)]) -> Bool {
reportParams.allSatisfy { reportId, minPubkeyExpiry in
reports.contains { $0.fogReportID == reportId && $0.pubkeyExpiry >= minPubkeyExpiry }
}
}
}

View File

@ -0,0 +1,72 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
final class FogResolver {
private let ptr: OpaquePointer
convenience init() {
self.init(attestation: Attestation())
}
convenience init(
attestation: Attestation,
reportUrlsAndResponses: [(FogUrl, Report_ReportResponse)]
) {
self.init(attestation: attestation)
for (reportUrl, response) in reportUrlsAndResponses {
addReportResponse(reportUrl: reportUrl, reportResponse: response)
}
}
private init(attestation: Attestation) {
logger.info("attestation: \(attestation)")
let verifier = AttestationVerifier(attestation: attestation)
// Safety: mc_fog_resolver_create should never return nil.
self.ptr = verifier.withUnsafeOpaquePointer { verifierPtr in
withMcInfallible {
mc_fog_resolver_create(verifierPtr)
}
}
}
deinit {
mc_fog_resolver_free(ptr)
}
func withUnsafeOpaquePointer<R>(_ body: (OpaquePointer) throws -> R) rethrows -> R {
try body(ptr)
}
private func addReportResponse(reportUrl: FogUrl, reportResponse: Report_ReportResponse) {
logger.info("")
let serializedReportResponse = reportResponse.serializedDataInfallible
serializedReportResponse.asMcBuffer { reportResponsePtr in
switch withMcError({ errorPtr in
mc_fog_resolver_add_report_response(
ptr,
reportUrl.url.absoluteString,
reportResponsePtr,
&errorPtr)
}) {
case .success:
break
case .failure(let error):
switch error.errorCode {
case .invalidInput:
// Safety: mc_fog_resolver_add_report_response shouldn't fail deserialization
// since we just serialized it and roundtrip serialization should always
// succeed.
logger.fatalError("\(error)")
default:
// Safety: mc_fog_resolver_add_report_response should not throw non-documented
// errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: error)")
}
}
}
}
}

View File

@ -0,0 +1,75 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_arguments
import Foundation
import LibMobileCoin
final class FogResolverManager {
private let serialQueue: DispatchQueue
private let reportAttestation: Attestation
private let reportManager: FogReportManager
init(
fogReportAttestation: Attestation,
serviceProvider: ServiceProvider,
targetQueue: DispatchQueue?
) {
self.serialQueue = DispatchQueue(label: "com.mobilecoin.\(Self.self)", target: targetQueue)
self.reportAttestation = fogReportAttestation
self.reportManager =
FogReportManager(serviceProvider: serviceProvider, targetQueue: targetQueue)
}
func fogResolver(
addresses: [PublicAddress],
completion: @escaping (Result<FogResolver, ConnectionError>) -> Void
) {
logger.info("addresses: \(addresses.map { "\(redacting: $0)" })")
let reportUrls = Set(addresses.compactMap { $0.fogReportUrl })
reportUrls.mapAsync({ reportUrl, callback in
reportManager.reportResponse(for: reportUrl) {
callback($0.map { response in
(reportUrl, response)
})
}
}, serialQueue: serialQueue, completion: {
completion($0.map { reportUrlsAndResponses in
FogResolver(
attestation: self.reportAttestation,
reportUrlsAndResponses: reportUrlsAndResponses)
})
})
}
func fogResolver(
addresses: [PublicAddress],
desiredMinPubkeyExpiry: UInt64,
completion: @escaping (Result<FogResolver, ConnectionError>) -> Void
) {
logger.info("\(addresses.map { "\(redacting: $0)" }), " +
"desiredMinPubkeyExpiry: \(desiredMinPubkeyExpiry)")
let fogInfos = addresses.compactMap { $0.fogInfo }
let reportUrlsToFogInfos = Dictionary(grouping: fogInfos, by: { $0.reportUrl })
let reportUrlsToReportParams = reportUrlsToFogInfos.mapValues { fogInfos in
fogInfos.map { ($0.reportId, desiredMinPubkeyExpiry) }
}
reportUrlsToReportParams.mapAsync({ reportUrlToReportParams, callback in
let (reportUrl, reportParams) = reportUrlToReportParams
reportManager.reportResponse(for: reportUrl, reportParams: reportParams) {
callback($0.map { response in
(reportUrl, response)
})
}
}, serialQueue: serialQueue, completion: {
completion($0.map { reportUrlsAndResponses in
FogResolver(
attestation: self.reportAttestation,
reportUrlsAndResponses: reportUrlsAndResponses)
})
})
}
}

View File

@ -0,0 +1,23 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
struct DefaultFogQueryScalingStrategy: FogQueryScalingStrategy {
private static let MIN_SEARCH_KEYS_PER_QUERY = 10
private static let MAX_SEARCH_KEYS_PER_QUERY = 200
private static let SCALING_MULTIPLIER: Double = 3
func create() -> AnyInfiniteIterator<PositiveInt> {
var next = Self.MIN_SEARCH_KEYS_PER_QUERY
return AnyInfiniteIterator {
guard let current = PositiveInt(next) else {
// Safety: `next` should always be positive if we only ever increase in value.
logger.fatalError("PositiveInt.init returned nil. value: \(next)")
}
next = min(Int(Double(next) * Self.SCALING_MULTIPLIER), Self.MAX_SEARCH_KEYS_PER_QUERY)
return current
}
}
}

View File

@ -0,0 +1,9 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
protocol FogQueryScalingStrategy {
func create() -> AnyInfiniteIterator<PositiveInt>
}

View File

@ -0,0 +1,140 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
enum FogRngError: Error {
case invalidKey(String)
case unsupportedCryptoBoxVersion(String)
}
extension FogRngError: CustomStringConvertible {
var description: String {
"Fog Kex Rng error: " + {
switch self {
case .invalidKey(let reason):
return "Invalid key: \(reason)"
case .unsupportedCryptoBoxVersion(let reason):
return "Unsupported CryptoBox version: \(reason)"
}
}()
}
}
final class FogRng {
static func make(fogRngKey: FogRngKey, accountKey: AccountKey) -> Result<FogRng, FogRngError> {
make(fogRngKey: fogRngKey, subaddressViewPrivateKey: accountKey.subaddressViewPrivateKey)
}
static func make(fogRngKey: FogRngKey, subaddressViewPrivateKey: RistrettoPrivate)
-> Result<FogRng, FogRngError>
{
subaddressViewPrivateKey.asMcBuffer { viewPrivateKeyPtr in
fogRngKey.pubkey.asMcBuffer { pubkeyPtr in
withMcError { errorPtr in
mc_fog_rng_create(viewPrivateKeyPtr, pubkeyPtr, fogRngKey.version, &errorPtr)
}.mapError {
switch $0.errorCode {
case .invalidInput:
return .invalidKey("\(redacting: $0.description)")
case .unsupportedCryptoBoxVersion:
return .unsupportedCryptoBoxVersion("\(redacting: $0.description)")
default:
// Safety: mc_fog_rng_create should not throw non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}.map { ptr in
FogRng(ptr)
}
}
}
}
/// - Returns: `.failure` when the input is not deserializable.
static func make(serializedData: Data) -> Result<FogRng, FogRngError> {
serializedData.asMcBuffer { dataPtr in
withMcError { errorPtr in
mc_fog_rng_deserialize_proto(dataPtr, &errorPtr)
}.mapError {
switch $0.errorCode {
case .invalidInput:
return .invalidKey("\(redacting: $0.description)")
case .unsupportedCryptoBoxVersion:
return .unsupportedCryptoBoxVersion("\(redacting: $0.description)")
default:
// Safety: mc_fog_rng_deserialize_proto should not throw non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}.map { ptr in
FogRng(ptr)
}
}
private let ptr: OpaquePointer
private let outputSize: Int
private init(_ ptr: OpaquePointer) {
self.ptr = ptr
self.outputSize = withMcInfallibleReturningOptional {
let len = mc_fog_rng_get_output_len(ptr)
return len >= 0 ? len : nil
}
}
deinit {
mc_fog_rng_free(ptr)
}
var serializedData: Data {
Data(withMcMutableBufferInfallible: { bufferPtr in
mc_fog_rng_serialize_proto(ptr, bufferPtr)
})
}
func clone() -> FogRng {
// Safety: mc_fog_rng_clone should never return nil.
FogRng(withMcInfallible { mc_fog_rng_clone(ptr) })
}
var index: UInt64 {
withMcInfallibleReturningOptional {
let res = mc_fog_rng_index(ptr)
return res >= 0 ? UInt64(res) : nil
}
}
var output: Data {
Data(withFixedLengthMcMutableBufferInfallible: outputSize) { bufferPtr in
mc_fog_rng_peek(ptr, bufferPtr)
}
}
func outputs(count: Int) -> [Data] {
let rngCopy = clone()
return (0..<count).map { _ in
rngCopy.advance()
}
}
@discardableResult
func advance() -> Data {
Data(withFixedLengthMcMutableBufferInfallible: outputSize) { bufferPtr in
mc_fog_rng_advance(ptr, bufferPtr)
}
}
}
extension FogRng {
static func make(fogRngPubkey: KexRng_KexRngPubkey, accountKey: AccountKey)
-> Result<FogRng, FogRngError>
{
make(
fogRngKey: FogRngKey(fogRngPubkey),
subaddressViewPrivateKey: accountKey.subaddressViewPrivateKey)
}
}

View File

@ -0,0 +1,34 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct FogRngKey {
let pubkey: Data
let version: UInt32
init(pubkey: Data, version: UInt32) {
self.pubkey = pubkey
self.version = version
}
}
extension FogRngKey: Equatable {}
extension FogRngKey: Hashable {}
extension FogRngKey {
init(_ pubkey: KexRng_KexRngPubkey) {
self.pubkey = pubkey.pubkey
self.version = pubkey.version
}
}
extension KexRng_KexRngPubkey {
init(_ fogRngKey: FogRngKey) {
self.init()
self.pubkey = fogRngKey.pubkey
self.version = fogRngKey.version
}
}

View File

@ -0,0 +1,349 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
final class FogRngSet {
private var ingestInvocationIdToRngTrackers: [Int64: RngTracker] = [:]
private(set) var rngRecordsKnownBlockCount: UInt64 = 0
var earliestRngRecordStartBlockIndex: UInt64? {
ingestInvocationIdToRngTrackers.values.map { $0.startBlockIndex }.min()
}
var knownBlockCount: UInt64 {
ingestInvocationIdToRngTrackers.values.map { $0.knownBlockCount }
.reduce(rngRecordsKnownBlockCount, min)
}
func searchAttempt(
targetBlockCount: UInt64?,
numOutputs: PositiveInt,
minOutputsPerSelectedRng: Int
) -> FogRngSetSearchAttempt {
// Max rngs we can select while maintaining the requested minimum outputs per selected rng.
let maxRngs = 0 < minOutputsPerSelectedRng && minOutputsPerSelectedRng <= numOutputs.value
? numOutputs.value / minOutputsPerSelectedRng : numOutputs.value
let selectedRngs =
selectRngsForSearch(requestedBlockCount: targetBlockCount, maxRngs: maxRngs)
guard !selectedRngs.isEmpty else {
logger.info(
"No active Fog rngs as of block count: \(rngRecordsKnownBlockCount)",
logFunction: false)
return FogRngSetSearchAttempt()
}
// Num of outputs to generate per selected rng.
let outputsPerRng = numOutputs.value / selectedRngs.count
let numRemainderOutputs = numOutputs.value % selectedRngs.count
let ingestInvocationIdAndRngSearchAttempt =
selectedRngs.enumerated().map { i, rngPair -> (Int64, FogRngSearchAttempt) in
let (ingestInvocationId, rngTracker) = rngPair
let numOutputs = outputsPerRng + (i < numRemainderOutputs ? 1 : 0)
let rngSearchAttempt = rngTracker.searchAttempt(numOutputs: numOutputs)
return (ingestInvocationId, rngSearchAttempt)
}
return FogRngSetSearchAttempt(
ingestInvocationIdToRngSearchAttempt: Dictionary(
uniqueKeysWithValues: ingestInvocationIdAndRngSearchAttempt))
}
private func selectRngsForSearch(requestedBlockCount: UInt64?, maxRngs: Int)
-> [Int64: RngTracker]
{
// Filter for rngs that are still active.
var eligibleRngTrackers = ingestInvocationIdToRngTrackers.filter { $0.value.active }
if let requestedBlockCount = requestedBlockCount {
// Filter for rngs that haven't already successfully processed the requested number of
// blocks.
//
// This is how we handle TxOut search pagination. If we repeatedly perform search
// attempts with the same `requestedBlockCount` (e.g. when performing a single balance
// check), eventually all rngs will have a `knownBlockCount` of at least
// `requestedBlockCount`.
eligibleRngTrackers = eligibleRngTrackers.filter {
$0.value.knownBlockCount < requestedBlockCount
}
}
return Dictionary(uniqueKeysWithValues: Array(eligibleRngTrackers.prefix(maxRngs)))
}
func processRngs(queryResponse: FogView_QueryResponse, accountKey: AccountKey)
-> Result<(), ConnectionError>
{
processRngRecords(
queryResponse.rngs,
highestProcessedBlockCount: queryResponse.highestProcessedBlockCount,
accountKey: accountKey
).map {
processDecommissionedRngs(queryResponse.decommissionedIngestInvocations)
}
}
private func processRngRecords(
_ rngRecords: [FogView_RngRecord],
highestProcessedBlockCount: UInt64,
accountKey: AccountKey
) -> Result<(), ConnectionError> {
for rngRecord in rngRecords
where ingestInvocationIdToRngTrackers[rngRecord.ingestInvocationID] == nil
{
logger.info(
"New RngRecord: ingestInvocationID: \(rngRecord.ingestInvocationID), pubkey: " +
"\(rngRecord.pubkey.pubkey.hexEncodedString()), version: " +
"\(rngRecord.pubkey.version), startBlockIndex: \(rngRecord.startBlock)",
logFunction: false)
switch RngTracker.make(rngRecord: rngRecord, accountKey: accountKey) {
case .success(let rngTracker):
ingestInvocationIdToRngTrackers[rngRecord.ingestInvocationID] = rngTracker
case .failure(let error):
switch error {
case .invalidKey(let reason):
let errorMessage = "Fog View returned invalid key rng key: \(reason)"
logger.error(errorMessage, logFunction: false)
return .failure(.invalidServerResponse(errorMessage))
case .unsupportedCryptoBoxVersion(let reason):
let errorMessage = "Fog View returned unsupported kex rng version: \(reason)"
logger.error(errorMessage, logFunction: false)
return .failure(.outdatedClient(errorMessage))
}
}
}
// Record that Fog has told us about all rngs that could possibly have been active up to
// `highestProcessedBlockCount` (while accounting for the possibility that we already have
// more up-to-date information already).
if highestProcessedBlockCount > rngRecordsKnownBlockCount {
logger.info(
"FogRngSet updating rngRecordsKnownBlockCount from \(rngRecordsKnownBlockCount) " +
"to \(highestProcessedBlockCount)",
logFunction: false)
rngRecordsKnownBlockCount = highestProcessedBlockCount
}
return .success(())
}
private func processDecommissionedRngs(
_ decommissionedRngs: [FogView_DecommissionedIngestInvocation]
) {
for decommissionedRng in decommissionedRngs {
if let rngTracker =
ingestInvocationIdToRngTrackers[decommissionedRng.ingestInvocationID]
{
logger.info(
"Rng decommissioned: ingestInvocationID: " +
"\(decommissionedRng.ingestInvocationID), lastIngestedBlockIndex: " +
"\(decommissionedRng.lastIngestedBlock)",
logFunction: false)
rngTracker.decommissioned = true
} else {
logger.error(
"Fog View decommissioned unknown ingestInvocation. ingestInvocationID: " +
"\(decommissionedRng.ingestInvocationID), lastIngestedBlock: " +
"\(decommissionedRng.lastIngestedBlock), current " +
"rngRecordsKnownBlockCount: \(rngRecordsKnownBlockCount)",
logFunction: false)
}
}
}
func processTxOutSearchResults(
queryResponse: FogView_QueryResponse,
rngSetSearchAttempt: FogRngSetSearchAttempt
) -> Result<[FogView_TxOutSearchResult], ConnectionError> {
processTxOutSearchResults(
queryResponse.txOutSearchResults,
highestProcessedBlockCount: queryResponse.highestProcessedBlockCount,
rngSetSearchAttempt: rngSetSearchAttempt)
}
private func processTxOutSearchResults(
_ txOutSearchResults: [FogView_TxOutSearchResult],
highestProcessedBlockCount: UInt64,
rngSetSearchAttempt: FogRngSetSearchAttempt
) -> Result<[FogView_TxOutSearchResult], ConnectionError> {
let searchKeyToTxOutResult = Dictionary(
txOutSearchResults.map { ($0.searchKey, $0) },
uniquingKeysWith: { key1, _ in key1 })
return rngSetSearchAttempt.ingestInvocationIdToRngSearchAttempt
.map { ingestInvocationId, rngSearchAttempt
-> Result<[FogView_TxOutSearchResult], ConnectionError> in
guard let rngTracker = ingestInvocationIdToRngTrackers[ingestInvocationId] else {
// This condition is considered a programming error and mean `searchAttempt` was
// created using a different `FogRngSet` instance. We silently fail here, since
// we know we're still in a valid, internally-consistent state.
logger.assertionFailure("RngTracker not found for rngKey in search attempt. " +
"ingestInvocationId: \(ingestInvocationId)")
return .success([])
}
// Filter for only the outputs we searched for.
let rngSearchKeyToTxOutResult: [Data: FogView_TxOutSearchResult] = Dictionary(
rngSearchAttempt.searchKeys.map { $0.bytes }.compactMap { searchKeyBytes in
guard let txOutResult = searchKeyToTxOutResult[searchKeyBytes] else {
logger.error(
"Searched key not in search results. searched key: " +
"\(searchKeyBytes.hexEncodedString())",
logFunction: false)
return nil
}
return (searchKeyBytes, txOutResult)
},
uniquingKeysWith: { key1, _ in key1 })
return rngTracker.processSearchKeyResults(
rngSearchKeyToTxOutResult: rngSearchKeyToTxOutResult,
highestProcessedBlockCount: highestProcessedBlockCount)
}.collectResult().map {
$0.flatMap { $0 }
}
}
}
private final class RngTracker {
let rng: FogRng
let startBlockIndex: UInt64
/// Whether this RNG is still in use by Fog.
///
/// If an RNG has been decommissioned, then all `TxOut`'s corresponding to the RNG are available
/// for immediate retrieval from Fog. This means that once we encounter a search miss we can
/// stop considering the RNG when generating search keys for a `TxOut` search.
var decommissioned = false
/// Whether we have found all `TxOut`'s for this RNG.
///
/// An RNG is active until the RNG has been both decommissioned and we've encountered at least
/// one search miss since.
var active = true
/// Number of blocks for which all `TxOut`s for this RNG are known.
///
/// Represents the number of blocks for which we can guarantee that all `TxOut`'s corresponding
/// to this RNG have been found. Put another way, we can guarantee that the next output from
/// this RNG has no corresponding `TxOut` within this block range.
///
/// This starts at either `0` or the RNG's `startBlock`. Each time we do a search, if we
/// encounter at least one miss (a.k.a. a `TxOut` is not found for an output from this RNG),
/// then we set this value to the `highestProcessedBlockCount` returned in the search response.
var knownBlockCount: UInt64
init(rng: FogRng, startBlockIndex: UInt64) {
self.rng = rng
self.startBlockIndex = startBlockIndex
// We assign a blockCount with the value of a blockIndex because, if X is the block index of
// the first block that the rng is active, then X is also the number of blocks that came
// before that block, hence our knownBlockCount. E.g. if the startBlockIndex is 1, 1 is also
// the number of blocks before block index 1.
self.knownBlockCount = startBlockIndex
}
func searchAttempt(numOutputs: Int) -> FogRngSearchAttempt {
let outputs = rng.outputs(count: numOutputs)
let searchKeys = outputs.map { FogSearchKey($0) }
// Note: converting directly from blockCount to blockIndex is valid here.
return FogRngSearchAttempt(searchKeys: searchKeys, startFromBlockIndex: knownBlockCount)
}
func processSearchKeyResults(
rngSearchKeyToTxOutResult: [Data: FogView_TxOutSearchResult],
highestProcessedBlockCount: UInt64
) -> Result<[FogView_TxOutSearchResult], ConnectionError> {
var foundTxOutResults: [FogView_TxOutSearchResult] = []
searchResultLoop: while true {
let output = rng.output
guard let txOutResult = rngSearchKeyToTxOutResult[output] else {
// Either we've found all the outputs we searched for or we've processed txos since
// this search attempt was made. Either way, if the next output we need wasn't one
// of the ones searched for or wasn't in the search results, then there's nothing
// else we can do with this rng.
logger.debug(
"Next rng output not found in searched keys. rng output: " +
"0x\(redacting: output.hexEncodedString())",
logFunction: false)
break
}
switch txOutResult.resultCodeEnum {
case .found:
foundTxOutResults.append(txOutResult)
rng.advance()
case .notFound:
// The search key failed to return a `TxOut` during this search attempt.
if highestProcessedBlockCount > knownBlockCount {
// `highestProcessedBlockCount` is the number of blocks that fog guarantees it
// finished processing when performing the search, so we store
// `highestProcessedBlockCount` as the `knownBlockCount` for this RNG on the
// assumption that if we encountered a miss for this RNG, then there are no more
// `TxOut`'s that can be found for this RNG in the first
// `highestProcessedBlockCount` number of blocks in the ledger.
knownBlockCount = highestProcessedBlockCount
}
// Break on the first miss
break searchResultLoop
case .rateLimited:
let errorMessage = "Fog View error response: rateLimited"
logger.warning(errorMessage, logFunction: false)
return .failure(.serverRateLimited(errorMessage))
case .badSearchKey, .internalError, .intentionallyUnused, .UNRECOGNIZED:
let errorMessage = "Fog view error response: \(txOutResult.resultCodeEnum), " +
"txOutResult.searchKey: \(redacting: txOutResult.searchKey)"
logger.error(errorMessage, logFunction: false)
return .failure(.invalidServerResponse(errorMessage))
}
}
return .success(foundTxOutResults)
}
}
extension RngTracker {
static func make(rngRecord: FogView_RngRecord, accountKey: AccountKey)
-> Result<RngTracker, FogRngError>
{
FogRng.make(fogRngPubkey: rngRecord.pubkey, accountKey: accountKey).map { rng in
RngTracker(rng: rng, rngRecord: rngRecord)
}
}
convenience init(rng: FogRng, rngRecord: FogView_RngRecord) {
self.init(rng: rng, startBlockIndex: rngRecord.startBlock)
}
}
struct FogRngSetSearchAttempt {
fileprivate let ingestInvocationIdToRngSearchAttempt: [Int64: FogRngSearchAttempt]
fileprivate init(ingestInvocationIdToRngSearchAttempt: [Int64: FogRngSearchAttempt]? = nil) {
self.ingestInvocationIdToRngSearchAttempt = ingestInvocationIdToRngSearchAttempt ?? [:]
}
var searchKeys: [FogSearchKey] {
ingestInvocationIdToRngSearchAttempt.values.flatMap { $0.searchKeys }
}
var lowestStartFromBlockIndex: UInt64 {
ingestInvocationIdToRngSearchAttempt.values.map { $0.startFromBlockIndex }.min() ?? 0
}
}
private struct FogRngSearchAttempt {
let searchKeys: [FogSearchKey]
let startFromBlockIndex: UInt64
}

View File

@ -0,0 +1,16 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
struct FogSearchKey {
let bytes: Data
init(_ bytes: Data) {
self.bytes = bytes
}
}
extension FogSearchKey: Equatable {}
extension FogSearchKey: Hashable {}

View File

@ -0,0 +1,123 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable closure_body_length multiline_function_chains
import Foundation
import LibMobileCoin
import os
extension FogView {
struct TxOutFetcher {
private let serialQueue: DispatchQueue
private let fogView: ReadWriteDispatchLock<FogView>
private let accountKey: AccountKey
private let fogViewService: FogViewService
private let fogQueryScalingStrategy: FogQueryScalingStrategy
init(
fogView: ReadWriteDispatchLock<FogView>,
accountKey: AccountKey,
fogViewService: FogViewService,
fogQueryScalingStrategy: FogQueryScalingStrategy,
targetQueue: DispatchQueue?
) {
self.serialQueue = DispatchQueue(
label: "com.mobilecoin.\(FogView.self).\(Self.self)",
target: targetQueue)
self.fogView = fogView
self.accountKey = accountKey
self.fogViewService = fogViewService
self.fogQueryScalingStrategy = fogQueryScalingStrategy
}
private var allRngTxOutsFoundBlockCount: UInt64 {
fogView.readSync { $0.allRngTxOutsFoundBlockCount }
}
func fetchTxOuts(
partialResultsWithWriteLock: @escaping ([KnownTxOut]) -> Void,
completion: @escaping (Result<(), ConnectionError>) -> Void
) {
performSearchRound(
targetBlockCount: nil,
queryScaling: nil,
partialResultsWithWriteLock: partialResultsWithWriteLock,
completion: completion)
}
private func performSearchRound(
targetBlockCount: UInt64?,
queryScaling: AnyInfiniteIterator<PositiveInt>?,
partialResultsWithWriteLock: @escaping ([KnownTxOut]) -> Void,
completion: @escaping (Result<(), ConnectionError>) -> Void
) {
logger.info("Querying Fog View...", logFunction: false)
let queryScaling = queryScaling ?? fogQueryScalingStrategy.create()
let numOutputs = queryScaling.next()
let (requestWrapper, searchAttempt) = fogView.readSync {
$0.queryRequest(targetBlockCount: targetBlockCount, numOutputs: numOutputs)
}
fogViewService.query(requestWrapper: requestWrapper) {
let result = $0.flatMap { response in
self.fogView.writeSync {
$0.processQueryResponse(
response,
searchAttempt: searchAttempt,
accountKey: self.accountKey
).map { processResult -> UInt64? in
if !searchAttempt.searchKeys.isEmpty {
partialResultsWithWriteLock(processResult.newTxOuts)
}
return processResult.nextRoundTargetBlockCount
}
}
}
switch result {
case .success(let nextRoundTargetBlockCount):
if let nextRoundTargetBlockCount = nextRoundTargetBlockCount {
// Reset query scaling if we didn't search for anything last round.
let queryScaling = !searchAttempt.searchKeys.isEmpty ? queryScaling : nil
// Do another search round
self.performSearchRound(
targetBlockCount: nextRoundTargetBlockCount,
queryScaling: queryScaling,
partialResultsWithWriteLock: partialResultsWithWriteLock,
completion: completion)
} else {
// Search complete
completion(.success(()))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
}
extension FogView_QueryResponse: CustomRedactingStringConvertible {
var redactingDescription: String {
let hits = txOutSearchResults.filter { $0.resultCodeEnum == .found }
return """
FogView_QueryResponse:
rng record count: \(rngs.count)
TxOutResult count: \(redacting: txOutSearchResults.count)
TxOutResult success count: \(redacting: hits.count)
highestProcessedBlockCount: \(highestProcessedBlockCount)
highestProcessedBlockSignatureTimestamp: \(highestProcessedBlockSignatureTimestamp) \
\(Date(timeIntervalSince1970: TimeInterval(highestProcessedBlockSignatureTimestamp)))
decommissionedRngs: \(decommissionedIngestInvocations)
missedBlockRanges.count: \(missedBlockRanges.count)
missedBlockRanges: \(missedBlockRanges)
nextStartFromUserEventId: \(nextStartFromUserEventID)
lastKnownBlockCount: \(lastKnownBlockCount)
lastKnownBlockCumulativeTxoCount: \(lastKnownBlockCumulativeTxoCount)
txOutResults result codes: \(redacting: txOutSearchResults.map { $0.resultCode })
"""
}
}

View File

@ -0,0 +1,193 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
final class FogView {
private let rngSet = FogRngSet()
private(set) var unscannedMissedBlocksRanges: [Range<UInt64>] = []
/// See `FogUserEvent` in the Fog repo for a list of user events.
private(set) var nextStartFromUserEventId: Int64 = 0
var allRngTxOutsFoundBlockCount: UInt64 {
rngSet.knownBlockCount
}
var allRngRecordsKnownBlockCount: UInt64 {
rngSet.rngRecordsKnownBlockCount
}
func queryRequest(targetBlockCount: UInt64?, numOutputs: PositiveInt)
-> (FogViewQueryRequestWrapper, FogSearchAttempt)
{
let rngSetSearchAttempt = rngSet.searchAttempt(
targetBlockCount: targetBlockCount,
numOutputs: numOutputs,
minOutputsPerSelectedRng: min(2, numOutputs.value))
let searchAttempt = FogSearchAttempt(
rngSetSearchAttempt: rngSetSearchAttempt,
targetBlockCount: targetBlockCount)
var wrapper = FogViewQueryRequestWrapper()
wrapper.requestAad.startFromUserEventID = nextStartFromUserEventId
wrapper.requestAad.startFromBlockIndex = rngSetSearchAttempt.lowestStartFromBlockIndex
wrapper.request.getTxos = rngSetSearchAttempt.searchKeys.map { $0.bytes }
logger.info(
"Fog view query params: startFromUserEventID: " +
"\(wrapper.requestAad.startFromUserEventID), startFromBlockIndex: " +
"\(wrapper.requestAad.startFromBlockIndex)",
logFunction: false)
return (wrapper, searchAttempt)
}
func processQueryResponse(
_ queryResponse: FogView_QueryResponse,
searchAttempt: FogSearchAttempt,
accountKey: AccountKey
) -> Result<(newTxOuts: [KnownTxOut], nextRoundTargetBlockCount: UInt64?), ConnectionError> {
logger.info("Processing Fog View query response...", logFunction: false)
return rngSet.processRngs(queryResponse: queryResponse, accountKey: accountKey).map {
processMissedBlockRanges(queryResponse.missedBlockRanges)
if queryResponse.nextStartFromUserEventID > nextStartFromUserEventId {
// We set this only after we've processed the info in `QueryResponse` that's
// considered an event, which currently includes `NewRngRecord`,
// `DecommissionIngestInvocation`, and `MissingBlocks`. (See `FogUserEvent` in the
// Fog repo for the canonical list.)
nextStartFromUserEventId = queryResponse.nextStartFromUserEventID
}
}.flatMap {
rngSet.processTxOutSearchResults(
queryResponse: queryResponse,
rngSetSearchAttempt: searchAttempt.rngSetSearchAttempt)
}.flatMap { searchResults in
searchResults.map { searchResult in
Self.decryptSearchResult(searchResult, accountKey: accountKey)
}.collectResult()
}.flatMap { txOutRecords in
txOutRecords.map { txOutRecord in
LedgerTxOut.make(txOutRecord: txOutRecord, viewKey: accountKey.viewPrivateKey)
}.collectResult()
}.map { txOuts in
let foundTxOuts = Self.ownedTxOuts(validating: txOuts, accountKey: accountKey)
// After the first call we know the current number of blocks processed by the fog
// ingest server, so we'll use that to try to build a complete view of the ledger
// for our account up to this number of blocks. We keep using this
// `targetBlockCount` because the ledger is always growing and we have to stop and
// declare the balance check finished at some point.
let targetBlockCount =
searchAttempt.targetBlockCount ?? queryResponse.highestProcessedBlockCount
let performAdditionalSearchRounds = allRngTxOutsFoundBlockCount < targetBlockCount
let nextRoundTargetBlockCount = performAdditionalSearchRounds ? targetBlockCount : nil
return (foundTxOuts, nextRoundTargetBlockCount)
}
}
private func processMissedBlockRanges(_ missedBlockRanges: [FogCommon_BlockRange]) {
var missedBlocks = missedBlockRanges.map { $0.range }
if let earliestRngStartBlockIndex = rngSet.earliestRngRecordStartBlockIndex {
missedBlocks = missedBlocks.compactMap { range in
// Check that we don't view key scan missed blocks that occur before the first
// RngRecord's startBlock. This is a workaround for Fog Ingest needing to mark the
// blocks before the first run of Fog Ingest as missed blocks.
//
// This can be removed when Fog provides a guarantee that it won't report the blocks
// before Fog Ingest was run for the first time as missed.
guard range.lowerBound >= earliestRngStartBlockIndex else {
if range.upperBound <= earliestRngStartBlockIndex {
// Entire missed block range is before the earliest RngRecord's startBlock.
return nil
} else {
// Part of missed block range is before the ealiest RngRecord's startBlock,
// so we modify the missed blocks range.
return earliestRngStartBlockIndex..<range.upperBound
}
}
return range
}
}
unscannedMissedBlocksRanges.append(contentsOf: missedBlocks)
}
func markBlocksAsScanned(blockRanges: [Range<UInt64>]) {
for range in blockRanges {
unscannedMissedBlocksRanges.removeAll(where: { $0 == range })
}
}
private static func decryptSearchResult(
_ searchResult: FogView_TxOutSearchResult,
accountKey: AccountKey
) -> Result<FogView_TxOutRecord, ConnectionError> {
FogViewUtils.decryptTxOutRecord(
ciphertext: searchResult.ciphertext,
accountKey: accountKey
).mapError { error in
switch error {
case .invalidInput:
let errorMessage = "Could not decrypt TxOut returned from Fog View, ciphertext: " +
"\(redacting: searchResult.ciphertext.base64EncodedString()), error: " +
"\(error)"
logger.error(errorMessage)
return .invalidServerResponse(errorMessage)
case .unsupportedVersion:
let errorMessage = "Could not decrypt TxOut returned from Fog View, ciphertext: " +
"\(redacting: searchResult.ciphertext.base64EncodedString()), error: " +
"\(error)"
logger.error(errorMessage)
return .outdatedClient(errorMessage)
}
}
}
/// Filters out TxOuts that don't belong to this account.
private static func ownedTxOuts(
validating txOuts: [LedgerTxOut],
accountKey: AccountKey
) -> [KnownTxOut] {
let ownedTxOuts = txOuts.compactMap { txOut -> KnownTxOut? in
guard let knownTxOut = txOut.decrypt(accountKey: accountKey) else {
logger.warning(
"TxOut received from Fog View is not owned by this account. txOut: " +
"\(redacting: txOut.targetKey.data.hexEncodedString())",
logFunction: false)
return nil
}
return knownTxOut
}
return ownedTxOuts
}
}
struct FogSearchAttempt {
fileprivate let rngSetSearchAttempt: FogRngSetSearchAttempt
fileprivate let targetBlockCount: UInt64?
var searchKeys: [FogSearchKey] { rngSetSearchAttempt.searchKeys }
}
extension LedgerTxOut {
fileprivate static func make(txOutRecord: FogView_TxOutRecord, viewKey: RistrettoPrivate)
-> Result<LedgerTxOut, ConnectionError>
{
guard let ledgerTxOut = LedgerTxOut(txOutRecord, viewKey: viewKey) else {
let errorMessage = "Invalid TxOut returned from Fog View. TxOutRecord: " +
"\(redacting: txOutRecord.serializedDataInfallible.base64EncodedString())"
logger.error(errorMessage)
return .failure(.invalidServerResponse(errorMessage))
}
return .success(ledgerTxOut)
}
}

View File

@ -0,0 +1,39 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
enum FogViewUtils {
static func encryptTxOutRecord(
txOutRecord: FogView_TxOutRecord,
publicAddress: PublicAddress,
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)?,
rngContext: Any?
) -> Result<Data, InvalidInputError> {
VersionedCryptoBox.encrypt(
plaintext: txOutRecord.serializedDataInfallible,
publicKey: publicAddress.viewPublicKeyTyped,
rng: rng,
rngContext: rngContext)
}
static func decryptTxOutRecord(
ciphertext: Data,
accountKey: AccountKey
) -> Result<FogView_TxOutRecord, VersionedCryptoBoxError> {
VersionedCryptoBox.decrypt(
ciphertext: ciphertext,
privateKey: accountKey.subaddressViewPrivateKey
).flatMap { decrypted in
guard let txOutRecord = try? FogView_TxOutRecord(serializedData: decrypted) else {
return .failure(.invalidInput("FogView_TxOutRecord deserialization failed. " +
"serializedData: \(redacting: decrypted.base64EncodedString())"))
}
return .success(txOutRecord)
}
}
}

View File

@ -0,0 +1,48 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
public struct BlockMetadata {
public let index: UInt64
let timestampStatus: TimestampStatus?
public var timestamp: Date? {
switch timestampStatus {
case .known(timestamp: let timestamp):
return timestamp
case .none, .unavailable, .temporarilyUnknown:
return nil
}
}
init(index: UInt64, timestamp: Date?) {
let timestampStatus: TimestampStatus?
if let timestamp = timestamp {
timestampStatus = .known(timestamp: timestamp)
} else {
timestampStatus = nil
}
self.init(index: index, timestampStatus: timestampStatus)
}
init(index: UInt64, timestampStatus: TimestampStatus?) {
self.index = index
self.timestampStatus = timestampStatus
}
}
extension BlockMetadata: Equatable {}
extension BlockMetadata: Hashable {}
extension BlockMetadata {
enum TimestampStatus {
case known(timestamp: Date)
case unavailable
case temporarilyUnknown
}
}
extension BlockMetadata.TimestampStatus: Equatable {}
extension BlockMetadata.TimestampStatus: Hashable {}

View File

@ -0,0 +1,69 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct KeyImage {
let data32: Data32
init(_ data: Data32) {
self.data32 = data
}
enum SpentStatus {
case spent(block: BlockMetadata)
case unspent(knownToBeUnspentBlockCount: UInt64)
var nextKeyImageQueryBlockIndex: UInt64 {
switch self {
case .spent:
return 0
case .unspent(let knownToBeUnspentBlockCount):
return knownToBeUnspentBlockCount
}
}
/// - Returns: `nil` when `blockCount` exceeds our knowledge about the spent status.
func status(atBlockCount blockCount: UInt64) -> SpentStatus? {
switch self {
case .spent(block: let spentAtBlock):
guard spentAtBlock.index < blockCount else {
return nil
}
return .spent(block: spentAtBlock)
case .unspent(knownToBeUnspentBlockCount: let knownToBeUnspentBlockCount):
guard knownToBeUnspentBlockCount >= blockCount else {
return nil
}
return .unspent(knownToBeUnspentBlockCount: blockCount)
}
}
}
}
extension KeyImage: DataConvertibleImpl {
typealias Iterator = Data.Iterator
init?(_ data: Data) {
guard let data32 = Data32(data.data) else {
return nil
}
self.init(data32)
}
var data: Data { data32.data }
}
extension KeyImage {
init?(_ keyImage: External_KeyImage) {
self.init(keyImage.data)
}
}
extension External_KeyImage {
init(_ keyImage: KeyImage) {
self.init(keyImage.data32)
}
}

View File

@ -0,0 +1,28 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
final class KeyImageSpentTracker {
let keyImage: KeyImage
var spentStatus: KeyImage.SpentStatus
init(_ keyImage: KeyImage) {
self.keyImage = keyImage
self.spentStatus = .unspent(knownToBeUnspentBlockCount: 0)
}
var isSpent: Bool {
if case .spent = spentStatus {
return true
} else {
return false
}
}
var nextKeyImageQueryBlockIndex: UInt64 {
spentStatus.nextKeyImageQueryBlockIndex
}
}

View File

@ -0,0 +1,40 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
struct KnownTxOut: TxOutProtocol {
private let ledgerTxOut: LedgerTxOut
let value: UInt64
let keyImage: KeyImage
init?(_ ledgerTxOut: LedgerTxOut, accountKey: AccountKey) {
guard let value = ledgerTxOut.value(accountKey: accountKey),
let keyImage = ledgerTxOut.keyImage(accountKey: accountKey),
let commitment = TxOutUtils.reconstructCommitment(
maskedValue: ledgerTxOut.maskedValue,
publicKey: ledgerTxOut.publicKey,
viewPrivateKey:accountKey.viewPrivateKey)
else {
return nil
}
self.commitment = commitment
self.ledgerTxOut = ledgerTxOut
self.value = value
self.keyImage = keyImage
}
var commitment: Data32
var maskedValue: UInt64 { ledgerTxOut.maskedValue }
var targetKey: RistrettoPublic { ledgerTxOut.targetKey }
var publicKey: RistrettoPublic { ledgerTxOut.publicKey }
var block: BlockMetadata { ledgerTxOut.block }
var globalIndex: UInt64 { ledgerTxOut.globalIndex }
}
extension KnownTxOut: Equatable {}
extension KnownTxOut: Hashable {}

View File

@ -0,0 +1,43 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct LedgerTxOut: TxOutProtocol {
private let txOut: PartialTxOut
let globalIndex: UInt64
let block: BlockMetadata
init(_ txOut: PartialTxOut, globalIndex: UInt64, block: BlockMetadata) {
self.txOut = txOut
self.globalIndex = globalIndex
self.block = block
}
var commitment: Data32 { txOut.commitment }
var maskedValue: UInt64 { txOut.maskedValue }
var targetKey: RistrettoPublic { txOut.targetKey }
var publicKey: RistrettoPublic { txOut.publicKey }
func decrypt(accountKey: AccountKey) -> KnownTxOut? {
KnownTxOut(self, accountKey: accountKey)
}
}
extension LedgerTxOut: Equatable {}
extension LedgerTxOut: Hashable {}
extension LedgerTxOut {
init?(_ txOutRecord: FogView_TxOutRecord, viewKey: RistrettoPrivate) {
guard let partialTxOut = PartialTxOut(txOutRecord, viewKey: viewKey) else {
return nil
}
let globalIndex = txOutRecord.txOutGlobalIndex
let block = BlockMetadata(
index: txOutRecord.blockIndex,
timestamp: txOutRecord.timestampDate)
self.init(partialTxOut, globalIndex: globalIndex, block: block)
}
}

View File

@ -0,0 +1,60 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct PartialTxOut: TxOutProtocol {
let commitment: Data32
let maskedValue: UInt64
let targetKey: RistrettoPublic
let publicKey: RistrettoPublic
}
extension PartialTxOut: Equatable {}
extension PartialTxOut: Hashable {}
extension PartialTxOut {
init(_ txOut: TxOut) {
self.init(
commitment: txOut.commitment,
maskedValue: txOut.maskedValue,
targetKey: txOut.targetKey,
publicKey: txOut.publicKey)
}
}
extension PartialTxOut {
init?(_ txOut: External_TxOut) {
guard let commitment = Data32(txOut.amount.commitment.data),
let targetKey = RistrettoPublic(txOut.targetKey.data),
let publicKey = RistrettoPublic(txOut.publicKey.data)
else {
return nil
}
self.init(
commitment: commitment,
maskedValue: txOut.amount.maskedValue,
targetKey: targetKey,
publicKey: publicKey)
}
init?(_ txOutRecord: FogView_TxOutRecord, viewKey: RistrettoPrivate) {
guard let targetKey = RistrettoPublic(txOutRecord.txOutTargetKeyData),
let publicKey = RistrettoPublic(txOutRecord.txOutPublicKeyData),
let commitment = TxOutUtils.reconstructCommitment(
maskedValue: txOutRecord.txOutAmountMaskedValue,
publicKey: publicKey,
viewPrivateKey: viewKey)
else {
return nil
}
self.init(
commitment: commitment,
maskedValue: txOutRecord.txOutAmountMaskedValue,
targetKey: targetKey,
publicKey: publicKey)
}
}

View File

@ -0,0 +1,83 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct TxOut: TxOutProtocol {
fileprivate let proto: External_TxOut
let commitment: Data32
let targetKey: RistrettoPublic
let publicKey: RistrettoPublic
/// - Returns: `nil` when the input is not deserializable.
init?(serializedData: Data) {
guard let proto = try? External_TxOut(serializedData: serializedData) else {
logger.warning(
"External_TxOut deserialization failed. serializedData: " +
"\(redacting: serializedData.base64EncodedString())",
logFunction: false)
return nil
}
switch TxOut.make(proto) {
case .success(let txOut):
self = txOut
case .failure(let error):
logger.warning(
"External_TxOut deserialization failed. serializedData: " +
"\(redacting: serializedData.base64EncodedString()), error: \(error)",
logFunction: false)
return nil
}
}
var serializedData: Data {
proto.serializedDataInfallible
}
var maskedValue: UInt64 { proto.amount.maskedValue }
var encryptedFogHint: Data { proto.eFogHint.data }
}
extension TxOut: Equatable {}
extension TxOut: Hashable {}
extension TxOut {
static func make(_ proto: External_TxOut) -> Result<TxOut, InvalidInputError> {
guard let commitment = Data32(proto.amount.commitment.data) else {
return .failure(
InvalidInputError("Failed parsing External_TxOut: invalid commitment format"))
}
guard let targetKey = RistrettoPublic(proto.targetKey.data) else {
return .failure(
InvalidInputError("Failed parsing External_TxOut: invalid target key format"))
}
guard let publicKey = RistrettoPublic(proto.publicKey.data) else {
return .failure(
InvalidInputError("Failed parsing External_TxOut: invalid public key format"))
}
return .success(
TxOut(proto: proto, commitment: commitment, targetKey: targetKey, publicKey: publicKey))
}
private init(
proto: External_TxOut,
commitment: Data32,
targetKey: RistrettoPublic,
publicKey: RistrettoPublic
) {
self.proto = proto
self.commitment = commitment
self.targetKey = targetKey
self.publicKey = publicKey
}
}
extension External_TxOut {
init(_ txOut: TxOut) {
self = txOut.proto
}
}

View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct TxOutMembershipProof {
let serializedData: Data
static func make(serializedData: Data) -> Result<TxOutMembershipProof, InvalidInputError> {
.success(TxOutMembershipProof(serializedData: serializedData))
}
private init(serializedData: Data) {
self.serializedData = serializedData
}
}
extension TxOutMembershipProof: Equatable {}
extension TxOutMembershipProof: Hashable {}
extension TxOutMembershipProof {
static func make(_ txOutMembershipProof: External_TxOutMembershipProof)
-> Result<TxOutMembershipProof, InvalidInputError>
{
let serializedData = txOutMembershipProof.serializedDataInfallible
return TxOutMembershipProof.make(serializedData: serializedData)
}
}

View File

@ -0,0 +1,66 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
protocol TxOutProtocol {
var commitment: Data32 { get }
var maskedValue: UInt64 { get }
var targetKey: RistrettoPublic { get }
var publicKey: RistrettoPublic { get }
}
extension TxOutProtocol {
func matches(accountKey: AccountKey) -> Bool {
TxOutUtils.matchesSubaddress(
targetKey: targetKey,
publicKey: publicKey,
viewPrivateKey: accountKey.viewPrivateKey,
subaddressSpendPrivateKey: accountKey.subaddressSpendPrivateKey)
}
func matchesAnySubaddress(accountKey: AccountKey) -> Bool {
TxOutUtils.matchesAnySubaddress(
maskedValue: maskedValue,
publicKey: publicKey,
viewPrivateKey: accountKey.viewPrivateKey)
}
func subaddressSpentPublicKey(viewPrivateKey: RistrettoPrivate) -> RistrettoPublic {
TxOutUtils.subaddressSpentPublicKey(
targetKey: targetKey,
publicKey: publicKey,
viewPrivateKey: viewPrivateKey)
}
/// - Returns: `nil` when `accountKey` cannot unmask value, either because `accountKey` does not
/// own `TxOut` or because ` TxOut` values are incongruent.
func value(accountKey: AccountKey) -> UInt64? {
TxOutUtils.value(
maskedValue: maskedValue,
publicKey: publicKey,
viewPrivateKey: accountKey.viewPrivateKey)
}
/// - Returns: `nil` when a valid `KeyImage` cannot be constructed, either because `accountKey`
/// does not own `TxOut` or because `TxOut` values are incongruent.
func keyImage(accountKey: AccountKey) -> KeyImage? {
TxOutUtils.keyImage(
targetKey: targetKey,
publicKey: publicKey,
viewPrivateKey: accountKey.viewPrivateKey,
subaddressSpendPrivateKey: accountKey.subaddressSpendPrivateKey)
}
}
extension FogView_TxOutRecord {
init(_ txOut: TxOutProtocol) {
self.init()
self.txOutAmountCommitmentData = txOut.commitment.data
self.txOutAmountMaskedValue = txOut.maskedValue
self.txOutTargetKeyData = txOut.targetKey.data
self.txOutPublicKeyData = txOut.publicKey.data
}
}

View File

@ -0,0 +1,259 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable closure_body_length
import Foundation
import LibMobileCoin
enum TxOutUtils {
static func matchesAnySubaddress(
maskedValue: UInt64,
publicKey: RistrettoPublic,
viewPrivateKey: RistrettoPrivate
) -> Bool {
var mcAmount = McTxOutAmount(masked_value: maskedValue)
return publicKey.asMcBuffer { publicKeyPtr in
viewPrivateKey.asMcBuffer { viewPrivateKeyPtr in
var matches = false
// Safety: mc_tx_out_matches_any_subaddress is infallible when preconditions are
// upheld.
withMcInfallible {
mc_tx_out_matches_any_subaddress(
&mcAmount,
publicKeyPtr,
viewPrivateKeyPtr,
&matches)
}
return matches
}
}
}
static func matchesSubaddress(
targetKey: RistrettoPublic,
publicKey: RistrettoPublic,
viewPrivateKey: RistrettoPrivate,
subaddressSpendPrivateKey: RistrettoPrivate
) -> Bool {
targetKey.asMcBuffer { targetKeyBufferPtr in
publicKey.asMcBuffer { publicKeyBufferPtr in
viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
subaddressSpendPrivateKey.asMcBuffer { spendPrivateKeyBufferPtr in
var matches = false
// Safety: mc_tx_out_matches_subaddress is infallible when preconditions are
// upheld.
withMcInfallible {
mc_tx_out_matches_subaddress(
targetKeyBufferPtr,
publicKeyBufferPtr,
viewKeyBufferPtr,
spendPrivateKeyBufferPtr,
&matches)
}
return matches
}
}
}
}
}
static func reconstructCommitment(
maskedValue: UInt64,
publicKey: RistrettoPublic,
viewPrivateKey: RistrettoPrivate
) -> Data32? {
var mcAmount = McTxOutAmount(masked_value: maskedValue)
return publicKey.asMcBuffer { publicKeyBufferPtr in
viewPrivateKey.asMcBuffer { viewPrivateKeyPtr in
switch Data32.make(withMcMutableBuffer: { bufferPtr, errorPtr in
mc_tx_out_reconstruct_commitment(
&mcAmount,
publicKeyBufferPtr,
viewPrivateKeyPtr,
bufferPtr,
&errorPtr)
}) {
case .success(let bytes):
// Safety: It's safe to skip validation because
// mc_tx_out_get_subaddress_spend_public_key should always return a valid
// RistrettoPublic on success.
return bytes as Data32
case .failure(let error):
switch error.errorCode {
case .invalidInput:
// Safety: This condition indicates a programming error and can only
// happen if arguments to mc_tx_out_get_subaddress_spend_public_key are
// supplied incorrectly.
// FIXME
logger.warning("error: \(redacting: error)")
return nil
default:
// Safety: mc_fog_resolver_add_report_response should not throw
// non-documented errors.
// FIXME
logger.warning("Unhandled LibMobileCoin error: \(redacting: error)")
return nil
}
}
}
}
}
static func subaddressSpentPublicKey(
targetKey: RistrettoPublic,
publicKey: RistrettoPublic,
viewPrivateKey: RistrettoPrivate
) -> RistrettoPublic {
targetKey.asMcBuffer { targetKeyBufferPtr in
publicKey.asMcBuffer { publicKeyBufferPtr in
viewPrivateKey.asMcBuffer { viewPrivateKeyPtr in
switch Data32.make(withMcMutableBuffer: { bufferPtr, errorPtr in
mc_tx_out_get_subaddress_spend_public_key(
targetKeyBufferPtr,
publicKeyBufferPtr,
viewPrivateKeyPtr,
bufferPtr,
&errorPtr)
}) {
case .success(let bytes):
// Safety: It's safe to skip validation because
// mc_tx_out_get_subaddress_spend_public_key should always return a valid
// RistrettoPublic on success.
return RistrettoPublic(skippingValidation: bytes)
case .failure(let error):
switch error.errorCode {
case .invalidInput:
// Safety: This condition indicates a programming error and can only
// happen if arguments to mc_tx_out_get_subaddress_spend_public_key are
// supplied incorrectly.
logger.fatalError("error: \(redacting: error)")
default:
// Safety: mc_fog_resolver_add_report_response should not throw
// non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: error)")
}
}
}
}
}
}
/// - Returns: `nil` when `viewPrivateKey` cannot unmask value, either because `viewPrivateKey`
/// does not own `TxOut` or because `TxOut` values are incongruent.
static func value(
maskedValue: UInt64,
publicKey: RistrettoPublic,
viewPrivateKey: RistrettoPrivate
) -> UInt64? {
var mcAmount = McTxOutAmount(masked_value: maskedValue)
return publicKey.asMcBuffer { publicKeyPtr in
viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
var valueOut: UInt64 = 0
switch withMcError({ errorPtr in
mc_tx_out_get_value(
&mcAmount,
publicKeyPtr,
viewKeyBufferPtr,
&valueOut,
&errorPtr)
}) {
case .success:
return valueOut
case .failure(let error):
switch error.errorCode {
case .transactionCrypto:
// Indicates either `commitment`/`maskedValue`/`publicKey` values are
// incongruent or `viewPrivateKey` does not own `TxOut`. However, it's
// not possible to determine which, only that the provided `commitment`
// doesn't match the computed commitment.
return nil
case .invalidInput:
// Safety: This condition indicates a programming error and can only
// happen if arguments to mc_tx_out_get_value are supplied incorrectly.
logger.fatalError("error: \(redacting: error)")
default:
// Safety: mc_tx_out_get_value should not throw non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: error)")
}
}
}
}
}
/// - Returns: `nil` when a valid `KeyImage` cannot be constructed, either because
/// `viewPrivateKey`/`subaddressSpendPrivateKey` do not own `TxOut` or because `TxOut`
/// values are incongruent.
static func keyImage(
targetKey: RistrettoPublic,
publicKey: RistrettoPublic,
viewPrivateKey: RistrettoPrivate,
subaddressSpendPrivateKey: RistrettoPrivate
) -> KeyImage? {
targetKey.asMcBuffer { targetKeyPtr in
publicKey.asMcBuffer { publicKeyPtr in
viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
subaddressSpendPrivateKey.asMcBuffer { spendKeyBufferPtr in
switch Data32.make(withMcMutableBuffer: { bufferPtr, errorPtr in
mc_tx_out_get_key_image(
targetKeyPtr,
publicKeyPtr,
viewKeyBufferPtr,
spendKeyBufferPtr,
bufferPtr,
&errorPtr)
}) {
case .success(let keyImageData):
return KeyImage(keyImageData)
case .failure(let error):
switch error.errorCode {
case .transactionCrypto:
// Indicates either `targetKey`/`publicKey` values are incongruent
// or`viewPrivateKey`/`subaddressSpendPrivateKey` does not own
// `TxOut`. However, it's not possible to determine which, only that
// the provided `targetKey` doesn't match the computed target key
// (aka onetime public key).
return nil
case .invalidInput:
// Safety: This condition indicates a programming error and can only
// happen if arguments to mc_tx_out_get_key_image are supplied
// incorrectly.
logger.fatalError("error: \(redacting: error)")
default:
// Safety: mc_tx_out_get_key_image should not throw non-documented
// errors.
logger.fatalError(
"Unhandled LibMobileCoin error: \(redacting: error)")
}
}
}
}
}
}
}
static func validateConfirmationNumber(
publicKey: RistrettoPublic,
confirmationNumber: TxOutConfirmationNumber,
viewPrivateKey: RistrettoPrivate
) -> Bool {
publicKey.asMcBuffer { publicKeyPtr in
confirmationNumber.asMcBuffer { confirmationNumberPtr in
viewPrivateKey.asMcBuffer { viewKeyBufferPtr in
var result = false
// Safety: mc_tx_out_validate_confirmation_number is infallible when
// preconditions are upheld.
withMcInfallible {
mc_tx_out_validate_confirmation_number(
publicKeyPtr,
confirmationNumberPtr,
viewKeyBufferPtr,
&result)
}
return result
}
}
}
}
}

View File

@ -0,0 +1,27 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
protocol CStructWrapper {
associatedtype CStruct
func withUnsafeCStructPointer<R>(
_ body: (UnsafePointer<CStruct>) throws -> R
) rethrows -> R
}
extension Optional where Wrapped: CStructWrapper {
func withUnsafeCStructPointer<R>(
_ body: (UnsafePointer<Wrapped.CStruct>?) throws -> R
) rethrows -> R {
if let unwrapped = self {
return try unwrapped.withUnsafeCStructPointer { ptr in
try body(ptr)
}
} else {
return try body(nil)
}
}
}

View File

@ -0,0 +1,125 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable colon multiline_function_chains
import Foundation
import LibMobileCoin
extension Data {
static func make(
withMcMutableBuffer body:
(UnsafeMutablePointer<McMutableBuffer>?, inout UnsafeMutablePointer<McError>?) -> Int
) -> Result<Data, LibMobileCoinError> {
withMcErrorReturningArrayCount { errorPtr in
// Call body() with nil to get number of bytes.
body(nil, &errorPtr)
}.flatMap { numBytes in
// Call body() again with a pointer to the output buffer.
var bytes = Data(repeating: 0, count: numBytes)
return bytes.asMcMutableBuffer { bufferPtr in
withMcErrorReturningArrayCount { errorPtr in
body(bufferPtr, &errorPtr)
}
}.map { numBytesWritten in
guard numBytesWritten <= numBytes else {
// This condition indicates a programming error.
logger.fatalError(
"numBytesWritten (\(numBytesWritten)) must be <= numBytes (\(numBytes))")
}
return bytes.prefix(numBytesWritten)
}
}
}
static func make(
withFixedLengthMcMutableBuffer numBytes: Int,
body: (UnsafeMutablePointer<McMutableBuffer>, inout UnsafeMutablePointer<McError>?) -> Bool
) -> Result<Data, LibMobileCoinError> {
var bytes = Data(repeating: 0, count: numBytes)
return bytes.asMcMutableBuffer { bufferPtr in
withMcError { errorPtr in
body(bufferPtr, &errorPtr)
}
}.map { bytes }
}
static func make(
withEstimatedLengthMcMutableBuffer numBytes: Int,
body: (UnsafeMutablePointer<McMutableBuffer>, inout UnsafeMutablePointer<McError>?) -> Int
) -> Result<Data, LibMobileCoinError> {
var bytes = Data(repeating: 0, count: numBytes)
return bytes.asMcMutableBuffer { bufferPtr in
withMcErrorReturningArrayCount { errorPtr in
body(bufferPtr, &errorPtr)
}
}.map { numBytesReturned in
guard numBytesReturned <= numBytes else {
// This condition indicates a programming error.
logger.fatalError(
"Number of bytes returned from LibMobileCoin (\(numBytesReturned)) is " +
"greater than estimated (\(numBytes))")
}
return bytes.prefix(numBytesReturned)
}
}
init(
withFixedLengthMcMutableBufferInfallible numBytes: Int,
body: (UnsafeMutablePointer<McMutableBuffer>) -> Bool
) {
self.init(repeating: 0, count: numBytes)
let success = asMcMutableBuffer { bufferPtr in
body(bufferPtr)
}
guard success else {
// This condition indicates a programming error.
logger.fatalError("Infallible LibMobileCoin function failed.")
}
}
init(withMcMutableBufferInfallible body: (UnsafeMutablePointer<McMutableBuffer>?) -> Int) {
// Call body() with nil to get number of bytes.
let numBytes = body(nil)
guard numBytes >= 0 else {
// This condition indicates a programming error.
logger.fatalError("Infallible LibMobileCoin function failed.")
}
var bytes = Data(repeating: 0, count: numBytes)
let numBytesWritten = bytes.asMcMutableBuffer { bufferPtr in
body(bufferPtr)
}
guard numBytesWritten >= 0 else {
// This condition indicates a programming error.
logger.fatalError("Infallible LibMobileCoin function failed.")
}
guard numBytesWritten <= numBytes else {
// This condition indicates a programming error.
logger.fatalError(
"numBytesWritten (\(numBytesWritten)) must be <= numBytes (\(numBytes))")
}
self = bytes.prefix(numBytesWritten)
}
init(
withEstimatedLengthMcMutableBufferInfallible numBytes: Int,
body: (UnsafeMutablePointer<McMutableBuffer>) -> Int
) {
var bytes = Data(repeating: 0, count: numBytes)
let numBytesReturned = bytes.asMcMutableBuffer(body)
guard numBytesReturned <= numBytes else {
// This condition indicates a programming error.
logger.fatalError(
"Number of bytes returned from LibMobileCoin \(numBytesReturned) is greater than " +
"estimated \(numBytes)")
}
self = bytes.prefix(numBytesReturned)
}
}

View File

@ -0,0 +1,69 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable colon multiline_function_chains
import Foundation
import LibMobileCoin
extension Data32 {
static func make(
withMcMutableBuffer body:
(UnsafeMutablePointer<McMutableBuffer>, inout UnsafeMutablePointer<McError>?) -> Bool
) -> Result<Data32, LibMobileCoinError> {
var bytes = Data32()
return bytes.asMcMutableBuffer { bufferPtr in
withMcError { errorPtr in
body(bufferPtr, &errorPtr)
}
}.map { bytes }
}
static func make(
withMcMutableBuffer body:
(UnsafeMutablePointer<McMutableBuffer>, inout UnsafeMutablePointer<McError>?) -> Int
) -> Result<Data32, LibMobileCoinError> {
var bytes = Data32()
return bytes.asMcMutableBuffer { bufferPtr in
withMcErrorReturningArrayCount { errorPtr in
body(bufferPtr, &errorPtr)
}
}.map { numBytesReturned in
guard numBytesReturned == 32 else {
// This condition indicates a programming error.
logger.fatalError(
"LibMobileCoin function returned unexpected byte count " +
"(\(numBytesReturned)). Expected 32.")
}
return bytes
}
}
init(withMcMutableBufferInfallible body: (UnsafeMutablePointer<McMutableBuffer>) -> Bool) {
self.init()
asMcMutableBuffer { bufferPtr in
guard body(bufferPtr) else {
// This condition indicates a programming error.
logger.fatalError("Infallible LibMobileCoin function failed.")
}
}
}
init(withMcMutableBufferInfallible body: (UnsafeMutablePointer<McMutableBuffer>) -> Int) {
self.init()
let numBytesReturned = asMcMutableBuffer { bufferPtr in
body(bufferPtr)
}
guard numBytesReturned > 0 else {
// This condition indicates a programming error.
logger.fatalError("Infallible LibMobileCoin function failed.")
}
guard numBytesReturned == 32 else {
// This condition indicates a programming error.
logger.fatalError(
"LibMobileCoin function returned unexpected byte count (\(numBytesReturned)). " +
"Expected 32.")
}
}
}

View File

@ -0,0 +1,35 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
struct LibMobileCoinError: Error {
static func make(consuming error: UnsafeMutablePointer<McError>)
-> Result<LibMobileCoinError, InvalidInputError>
{
defer {
mc_error_free(error)
}
guard let libMcError = LibMobileCoinError(error.pointee) else {
return .failure(InvalidInputError(
"Unknown LibMobileCoin error code: \(error.pointee.error_code), description: " +
"\(String(cString: error.pointee.error_description))"))
}
return .success(libMcError)
}
let errorCode: McErrorCode
let description: String
/// - Returns: `nil` when the error kind is unrecognized.
init?(_ error: McError) {
self.description = String(cString: error.error_description)
guard let errorCode = McErrorCode(rawValue: error.error_code) else {
return nil
}
self.errorCode = errorCode
}
}

View File

@ -0,0 +1,204 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
enum McConstants {}
// MARK: - Transaction
extension McConstants {
/// Each input ring must contain this many elements.
static let RING_SIZE = 11
/// Each transaction must contain no more than this many inputs (rings).
static let MAX_INPUTS = 16
/// Each transaction must contain no more than this many outputs.
static let MAX_OUTPUTS = 16
/// Maximum number of blocks in the future a transaction's tombstone block can be set to.
static let MAX_TOMBSTONE_BLOCKS: UInt64 = 100
/// Minimum allowed fee, denominated in picoMOB.
static let DEFAULT_MINIMUM_FEE: UInt64 = 10_000_000_000
/// Transaction hash length, in bytes.
static let TX_HASH_LEN = 32
/// Length of a Transaction's encrypted fog hint field, in bytes.
static let ENCRYPTED_FOG_HINT_LEN = 128
/// Length of a Transaction confirmation number, in bytes.
static let CONFIRMATION_NUMBER_LEN = 32
}
// MARK: - Block
extension McConstants {
/// Maximum number of transactions that may be included in a Block.
static let MAX_TRANSACTIONS_PER_BLOCK = 5000
}
// MARK: - TxOut
extension McConstants {
/// Length of a TxOut key image, in bytes.
static let KEY_IMAGE_LEN = 32
}
// MARK: - MOB
extension McConstants {
/// The MobileCoin network will contain a fixed supply of 250 million mobilecoins (MOB).
static let TOTAL_MOB: UInt64 = 250_000_000
}
// MARK: - Account key
extension McConstants {
/// Length of the root entropy used to construct an account key, in bytes.
static let ROOT_ENTROPY_LEN = 32
/// An account's "default address" is its zero^th subaddress.
static let DEFAULT_SUBADDRESS_INDEX: UInt64 = 0
}
// MARK: - Keys
extension McConstants {
/// Ristretto private key length, in bytes.
static let RISTRETTO_PRIVATE_LEN = 32
/// Ristretto public key length, in bytes.
static let RISTRETTO_PUBLIC_LEN = 32
/// The length of a curve25519 EdDSA `Signature`, in bytes.
static let SCHNORRKEL_SIGNATURE_LEN = 64
}
// MARK: - Attestation
extension McConstants {
/// MRENCLAVE length, in bytes.
static let MRENCLAVE_LEN = 32
/// MRSIGNER length, in bytes.
static let MRSIGNER_LEN = 32
}
// MARK: - Enclave
extension McConstants {
static let CONSENSUS_PRODUCT_ID: UInt16 = 1
static let FOG_VIEW_PRODUCT_ID: UInt16 = 3
static let FOG_LEDGER_PRODUCT_ID: UInt16 = 2
static let FOG_REPORT_PRODUCT_ID: UInt16 = 4
static let CONSENSUS_SECURITY_VERSION: UInt16 = 1
static let DEV_CONSENSUS_MRSIGNER_HEX =
"7ee5e29d74623fdbc6fbf1454be6f3bb0b86c12366b7b478ad13353e44de8411"
static let DEV_CONSENSUS_MRSIGNER = Data([
126, 229, 226, 157, 116, 98, 63, 219, 198, 251, 241, 69, 75, 230, 243, 187, 11, 134, 193,
35, 102, 183, 180, 120, 173, 19, 53, 62, 68, 222, 132, 17,
])
static let TESTNET_CONSENSUS_MRSIGNER_HEX =
"bf7fa957a6a94acb588851bc8767e0ca57706c79f4fc2aa6bcb993012c3c386c"
static let TESTNET_CONSENSUS_MRSIGNER = Data([
191, 127, 169, 87, 166, 169, 74, 203, 88, 136, 81, 188, 135, 103, 224, 202, 87, 112, 108,
121, 244, 252, 42, 166, 188, 185, 147, 1, 44, 60, 56, 108,
])
static let FOG_VIEW_SECURITY_VERSION: UInt16 = 1
static let FOG_LEDGER_SECURITY_VERSION: UInt16 = 1
static let DEV_FOG_MRSIGNER_HEX =
"7ee5e29d74623fdbc6fbf1454be6f3bb0b86c12366b7b478ad13353e44de8411"
static let DEV_FOG_MRSIGNER = Data([
126, 229, 226, 157, 116, 98, 63, 219, 198, 251, 241, 69, 75, 230, 243, 187, 11, 134, 193,
35, 102, 183, 180, 120, 173, 19, 53, 62, 68, 222, 132, 17,
])
static let TESTNET_FOG_MRSIGNER_HEX =
"bf7fa957a6a94acb588851bc8767e0ca57706c79f4fc2aa6bcb993012c3c386c"
static let TESTNET_FOG_MRSIGNER = Data([
191, 127, 169, 87, 166, 169, 74, 203, 88, 136, 81, 188, 135, 103, 224, 202, 87, 112, 108,
121, 244, 252, 42, 166, 188, 185, 147, 1, 44, 60, 56, 108,
])
static let FOG_REPORT_SECURITY_VERSION: UInt16 = 1
static let DEV_FOG_REPORT_MRSIGNER_HEX =
"7ee5e29d74623fdbc6fbf1454be6f3bb0b86c12366b7b478ad13353e44de8411"
static let DEV_FOG_REPORT_MRSIGNER = Data([
126, 229, 226, 157, 116, 98, 63, 219, 198, 251, 241, 69, 75, 230, 243, 187, 11, 134, 193,
35, 102, 183, 180, 120, 173, 19, 53, 62, 68, 222, 132, 17,
])
static let TESTNET_FOG_REPORT_MRSIGNER_HEX =
"bf7fa957a6a94acb588851bc8767e0ca57706c79f4fc2aa6bcb993012c3c386c"
static let TESTNET_FOG_REPORT_MRSIGNER = Data([
191, 127, 169, 87, 166, 169, 74, 203, 88, 136, 81, 188, 135, 103, 224, 202, 87, 112, 108,
121, 244, 252, 42, 166, 188, 185, 147, 1, 44, 60, 56, 108,
])
}
// MARK: - Url
extension McConstants {
static let MOB_URI_SCHEME = "mob"
/// The part before the '://' of a URL.
static let CONSENSUS_SCHEME_SECURE = "mc"
static let CONSENSUS_SCHEME_INSECURE = "insecure-mc"
/// Default port numbers
static let CONSENSUS_DEFAULT_SECURE_PORT = 443
static let CONSENSUS_DEFAULT_INSECURE_PORT = 3223
/// The part before the '://' of a URL.
static let FOG_SCHEME_SECURE = "fog"
static let FOG_SCHEME_INSECURE = "insecure-fog"
/// Default port numbers
static let FOG_DEFAULT_SECURE_PORT = 443
static let FOG_DEFAULT_INSECURE_PORT = 3225
}
// MARK: - Fog Report
extension McConstants {
/// Fog authority subjectPublicKeyInfo used in development, in hexidecimal.
static let DEV_FOG_AUTHORITY_SPKI_HEX = "23e9dfabdaf74c69428ec0dfac15784eedc7466e"
/// Fog authority subjectPublicKeyInfo used in development, in bytes.
static let DEV_FOG_AUTHORITY_SPKI = Data([
35, 233, 223, 171, 218, 247, 76, 105, 66, 142, 192, 223, 172, 21, 120, 78, 237, 199, 70,
110,
])
}
// MARK: - Fog Ledger
extension McConstants {
/// Maximum number of Key Images that may be checked in a single request.
static let FOG_KEY_IMAGE_MAX_REQUEST_SIZE = 2000
}

View File

@ -0,0 +1,35 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
final class McData {
let ptr: OpaquePointer
init(_ ptr: OpaquePointer) {
self.ptr = ptr
}
deinit {
mc_data_free(ptr)
}
var bytes: Data {
Data(withMcMutableBufferInfallible: { bufferPtr in
mc_data_get_bytes(ptr, bufferPtr)
})
}
}
extension Data {
static func make(withMcDataBytes body: (inout UnsafeMutablePointer<McError>?) -> OpaquePointer?)
-> Result<Data, LibMobileCoinError>
{
withMcError(body).map {
let mcData = McData($0)
return mcData.bytes
}
}
}

View File

@ -0,0 +1,119 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
func withMcInfallible(_ body: () -> OpaquePointer?) -> OpaquePointer {
guard let value = body() else {
logger.fatalError("Error: \(#function): Infallible LibMobileCoin function failed")
}
return value
}
func withMcError(_ body: (inout UnsafeMutablePointer<McError>?) -> OpaquePointer?)
-> Result<OpaquePointer, LibMobileCoinError>
{
var error: UnsafeMutablePointer<McError>?
guard let value = body(&error) else {
guard let mcError = error else {
// Safety: This condition should never occur and indicates a programming error.
logger.fatalError("Error: \(#function): block returned failure but out_error == NULL.")
}
let err: LibMobileCoinError
do {
err = try LibMobileCoinError.make(consuming: mcError).get()
} catch {
logger.fatalError("Error: \(#function): \(error)")
}
guard err.errorCode != .panic else {
logger.fatalError("LibMobileCoin function panicked: \(redacting: err.description)")
}
return .failure(err)
}
return .success(value)
}
func withMcInfallible(_ body: () -> Bool) {
guard body() else {
logger.fatalError("Error: \(#function): Infallible LibMobileCoin function failed.")
}
}
func withMcError(_ body: (inout UnsafeMutablePointer<McError>?) -> Bool)
-> Result<(), LibMobileCoinError>
{
var error: UnsafeMutablePointer<McError>?
guard body(&error) else {
guard let mcError = error else {
// Safety: This condition should never occur and indicates a programming error.
logger.fatalError("Error: \(#function): block returned failure but out_error == NULL.")
}
let err: LibMobileCoinError
do {
err = try LibMobileCoinError.make(consuming: mcError).get()
} catch {
logger.fatalError("Error: \(#function): \(error)")
}
guard err.errorCode != .panic else {
logger.fatalError("LibMobileCoin function panicked: \(redacting: err.description)")
}
return .failure(err)
}
return .success(())
}
func withMcInfallibleReturningOptional<T>(_ body: () -> T?) -> T {
guard let value = body() else {
logger.fatalError("Error: \(#function): Infallible LibMobileCoin function failed.")
}
return value
}
func withMcErrorReturningOptional<T>(_ body: (inout UnsafeMutablePointer<McError>?) -> T?)
-> Result<T, LibMobileCoinError>
{
var error: UnsafeMutablePointer<McError>?
guard let value = body(&error) else {
guard let mcError = error else {
// Safety: This condition should never occur and indicates a programming error.
logger.fatalError("Error: \(#function): block returned failure but out_error == NULL.")
}
let err: LibMobileCoinError
do {
err = try LibMobileCoinError.make(consuming: mcError).get()
} catch {
logger.fatalError("Error: \(#function): \(error)")
}
guard err.errorCode != .panic else {
logger.fatalError("LibMobileCoin function panicked: \(redacting: err.description)")
}
return .failure(err)
}
return .success(value)
}
func withMcErrorReturningArrayCount(_ body: (inout UnsafeMutablePointer<McError>?) -> Int)
-> Result<Int, LibMobileCoinError>
{
var error: UnsafeMutablePointer<McError>?
let value = body(&error)
guard value >= 0 else {
guard let mcError = error else {
// Safety: This condition should never occur and indicates a programming error.
logger.fatalError("Error: \(#function): block returned failure but out_error == NULL.")
}
let err: LibMobileCoinError
do {
err = try LibMobileCoinError.make(consuming: mcError).get()
} catch {
logger.fatalError("Error: \(#function): \(error)")
}
guard err.errorCode != .panic else {
logger.fatalError("LibMobileCoin function panicked: \(redacting: err.description)")
}
return .failure(err)
}
return .success(value)
}

View File

@ -0,0 +1,22 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
func withMcRngCallback<T>(
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)?,
rngContext: Any?,
_ body: (UnsafeMutablePointer<McRngCallback>?) throws -> T
) rethrows -> T {
if let rng = rng {
var rngContext = rngContext
return try withUnsafeMutablePointer(to: &rngContext) { rngContextPtr in
var rngCallback = McRngCallback(rng: rng, context: rngContextPtr)
return try body(&rngCallback)
}
} else {
return try body(nil)
}
}

View File

@ -0,0 +1,17 @@
// swiftlint:disable:this file_name
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
extension String {
init(mcString: UnsafeMutablePointer<CChar>) {
defer {
mc_string_free(mcString)
}
self.init(cString: mcString)
}
}

View File

@ -0,0 +1,11 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable orphaned_doc_comment
import LibMobileCoin
/// This file contains temporary interop code to allow easy source compatibility with upstream
/// LibMobileCoin and MobileCoin server code. The code in this file can be removed once upstream
/// is deployed to alpha and the older server code no longer needs to be supported.

View File

@ -0,0 +1,183 @@
// swiftlint:disable:this file_name
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
// MARK: - External
extension External_RistrettoPrivate {
init<DataType: DataConvertible>(_ data: DataType) {
self.init()
self.data = data.data
}
}
extension External_CompressedRistretto {
init<DataType: DataConvertible>(_ data: DataType) {
self.init()
self.data = data.data
}
}
extension External_KeyImage {
init<DataType: DataConvertible>(_ data: DataType) {
self.init()
self.data = data.data
}
}
extension External_Amount {
init<CommitmentType: DataConvertible>(commitment: CommitmentType, maskedValue: UInt64) {
self.init()
self.commitment = External_CompressedRistretto(commitment)
self.maskedValue = maskedValue
}
}
extension External_EncryptedFogHint {
init<DataType: DataConvertible>(_ data: DataType) {
self.init()
self.data = data.data
}
}
// MARK: - Fog Common
extension FogCommon_BlockRange {
init(_ range: Range<UInt64>) {
self.init()
self.startBlock = range.lowerBound
self.endBlock = range.upperBound
}
var range: Range<UInt64> {
get { startBlock..<endBlock }
set {
startBlock = newValue.lowerBound
endBlock = newValue.upperBound
}
}
}
// MARK: - Fog View
extension FogView_RngRecord {
init(nonce fogRngKey: FogRngKey, startBlock: UInt64) {
self.init()
self.pubkey = KexRng_KexRngPubkey(fogRngKey)
self.startBlock = startBlock
}
}
extension FogView_TxOutSearchResult {
var resultCodeEnum: FogView_TxOutSearchResultCode {
get {
FogView_TxOutSearchResultCode(rawValue: Int(resultCode))
?? .UNRECOGNIZED(Int(resultCode))
}
set { resultCode = UInt32(newValue.rawValue) }
}
}
extension FogView_TxOutRecord {
var timestampDate: Date? {
get { timestamp != UInt64.max ? Date(timeIntervalSince1970: TimeInterval(timestamp)) : nil }
set {
if let newValue = newValue {
timestamp = UInt64(newValue.timeIntervalSince1970)
} else {
timestamp = UInt64.max
}
}
}
}
// MARK: - Fog Ledger
extension FogLedger_OutputResult {
var resultCodeEnum: FogLedger_OutputResultCode {
get {
FogLedger_OutputResultCode(rawValue: Int(resultCode)) ?? .UNRECOGNIZED(Int(resultCode))
}
set { resultCode = UInt32(newValue.rawValue) }
}
}
extension FogLedger_KeyImageResult {
var timestampDate: Date {
get { Date(timeIntervalSince1970: TimeInterval(timestamp)) }
set { timestamp = UInt64(newValue.timeIntervalSince1970) }
}
var timestampResultCodeEnum: Watcher_TimestampResultCode {
get {
Watcher_TimestampResultCode(rawValue: Int(timestampResultCode))
?? .UNRECOGNIZED(Int(timestampResultCode))
}
set { timestampResultCode = UInt32(newValue.rawValue) }
}
var keyImageResultCodeEnum: FogLedger_KeyImageResultCode {
get {
FogLedger_KeyImageResultCode(rawValue: Int(keyImageResultCode))
?? .UNRECOGNIZED(Int(keyImageResultCode))
}
set { keyImageResultCode = UInt32(newValue.rawValue) }
}
var timestampStatus: BlockMetadata.TimestampStatus? {
switch timestampResultCodeEnum {
case .timestampFound:
return .known(timestamp: timestampDate)
case .unavailable:
return .unavailable
case .watcherBehind, .watcherDatabaseError, .blockIndexOutOfBounds:
return .temporarilyUnknown
case .unusedField, .UNRECOGNIZED:
return nil
}
}
}
extension FogLedger_BlockRequest {
var rangeValues: [Range<UInt64>] {
get { ranges.map { $0.startBlock..<$0.endBlock } }
set { ranges = newValue.map { FogCommon_BlockRange($0) } }
}
}
extension FogLedger_BlockData {
var timestampDate: Date {
get { Date(timeIntervalSince1970: TimeInterval(timestamp)) }
set { timestamp = UInt64(newValue.timeIntervalSince1970) }
}
var timestampResultCodeEnum: Watcher_TimestampResultCode {
get {
Watcher_TimestampResultCode(rawValue: Int(timestampResultCode))
?? .UNRECOGNIZED(Int(timestampResultCode))
}
set { timestampResultCode = UInt32(newValue.rawValue) }
}
var timestampStatus: BlockMetadata.TimestampStatus? {
switch timestampResultCodeEnum {
case .timestampFound:
return .known(timestamp: timestampDate)
case .unavailable:
return .unavailable
case .watcherBehind, .watcherDatabaseError, .blockIndexOutOfBounds:
return .temporarilyUnknown
case .unusedField, .UNRECOGNIZED:
return nil
}
}
var metadata: BlockMetadata {
BlockMetadata(index: index, timestampStatus: timestampStatus)
}
}

View File

@ -0,0 +1,50 @@
// swiftlint:disable:this file_name
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
// MARK: - External
extension External_AccountKey: InfallibleDataSerializable {}
extension External_PublicAddress: InfallibleDataSerializable {}
extension External_TxOutMembershipProof: InfallibleDataSerializable {}
extension External_TxOut: InfallibleDataSerializable {}
extension External_Tx: InfallibleDataSerializable {}
extension External_Receipt: InfallibleDataSerializable {}
// MARK: - Printable
extension Printable_PrintableWrapper: InfallibleDataSerializable {}
// MARK: - Attest
extension Attest_Message: InfallibleDataSerializable {}
// MARK: - Fog Report
extension Report_ReportResponse: InfallibleDataSerializable {}
// MARK: - Fog View
extension FogView_QueryRequestAAD: InfallibleDataSerializable {}
extension FogView_QueryRequest: InfallibleDataSerializable {}
extension FogView_QueryResponse: InfallibleDataSerializable {}
extension FogView_TxOutRecord: InfallibleDataSerializable {}
// MARK: - Fog Ledger
extension FogLedger_GetOutputsRequest: InfallibleDataSerializable {}
extension FogLedger_GetOutputsResponse: InfallibleDataSerializable {}
extension FogLedger_CheckKeyImagesRequest: InfallibleDataSerializable {}
extension FogLedger_CheckKeyImagesResponse: InfallibleDataSerializable {}

View File

@ -0,0 +1,50 @@
// swiftlint:disable:this file_name
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
extension DataConvertible {
func asMcBuffer<T>(_ body: (UnsafePointer<McBuffer>) throws -> T) rethrows -> T {
try data.withUnsafeBytes {
let ptr = $0.bindMemory(to: UInt8.self)
guard let bufferPtr = ptr.baseAddress else {
// This indicates a programming error. Pointer returned from withUnsafeBytes
// shouldn't have a nil baseAddress.
logger.fatalError("ptr.baseAddress == nil.")
}
var buffer = McBuffer(buffer: bufferPtr, len: ptr.count)
return try body(&buffer)
}
}
}
extension MutableData {
mutating func asMcMutableBuffer<T>(
_ body: (UnsafeMutablePointer<McMutableBuffer>) throws -> T
) rethrows -> T {
try withUnsafeMutableBytes {
let ptr = $0.bindMemory(to: UInt8.self)
guard let bufferPtr = ptr.baseAddress else {
// This indicates a programming error. Pointer returned from withUnsafeMutableBytes
// shouldn't have a nil baseAddress.
logger.fatalError("ptr.baseAddress == nil.")
}
var buffer = McMutableBuffer(buffer: bufferPtr, len: ptr.count)
return try body(&buffer)
}
}
}
extension Optional where Wrapped: DataConvertible {
func asOptMcBuffer<T>(_ body: (UnsafePointer<McBuffer>?) throws -> T) rethrows -> T {
if let unwrapped = self {
return try unwrapped.asMcBuffer(body)
} else {
return try body(nil)
}
}
}

View File

@ -0,0 +1,57 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
extension AccountKey {
public static func make(
entropy: Data,
fogReportUrl: String,
fogReportId: String,
fogAuthoritySpki: Data,
accountIndex: UInt32 = 0
) -> Result<AccountKey, InvalidInputError> {
Bip39Utils.mnemonic(fromEntropy: entropy).flatMap { mnemonic in
make(
mnemonic: mnemonic.phrase,
fogReportUrl: fogReportUrl,
fogReportId: fogReportId,
fogAuthoritySpki: fogAuthoritySpki,
accountIndex: accountIndex)
}
}
public static func make(
mnemonic: String,
fogReportUrl: String,
fogReportId: String,
fogAuthoritySpki: Data,
accountIndex: UInt32 = 0
) -> Result<AccountKey, InvalidInputError> {
Slip10Utils.accountPrivateKeys(fromMnemonic: mnemonic, accountIndex: accountIndex)
.flatMap {
AccountKey.make(
viewPrivateKey: $0.viewPrivateKey,
spendPrivateKey: $0.spendPrivateKey,
fogReportUrl: fogReportUrl,
fogReportId: fogReportId,
fogAuthoritySpki: fogAuthoritySpki)
}
}
init(
mnemonic: Mnemonic,
fogInfo: FogInfo? = nil,
accountIndex: UInt32 = 0,
subaddressIndex: UInt64 = McConstants.DEFAULT_SUBADDRESS_INDEX
) {
let (viewPrivateKey, spendPrivateKey) =
Slip10Utils.accountPrivateKeys(fromMnemonic: mnemonic, accountIndex: accountIndex)
self.init(
viewPrivateKey: viewPrivateKey,
spendPrivateKey: spendPrivateKey,
fogInfo: fogInfo,
subaddressIndex: subaddressIndex)
}
}

View File

@ -0,0 +1,75 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
enum Bip39Utils {
static func mnemonic(fromEntropy entropy: Data32) -> Mnemonic {
let mnemonic = entropy.asMcBuffer { entropyPtr in
String(mcString:
withMcInfallibleReturningOptional { mc_bip39_mnemonic_from_entropy(entropyPtr) })
}
return Mnemonic(phraseSkippingValidation: mnemonic)
}
/// Entropy must be a multiple of 4 bytes and 16-32 bytes in length.
static func mnemonic(fromEntropy entropy: Data) -> Result<Mnemonic, InvalidInputError> {
guard entropy.count % 4 == 0 else {
return .failure(InvalidInputError("BIP39 error: entropy must be a multiple of 4 bytes"))
}
guard entropy.count >= 16 && entropy.count <= 32 else {
return .failure(InvalidInputError(
"BIP39 error: entropy must be between 16 and 32 bytes, inclusive"))
}
let mnemonic = entropy.asMcBuffer { entropyPtr in
String(mcString:
withMcInfallibleReturningOptional { mc_bip39_mnemonic_from_entropy(entropyPtr) })
}
return .success(Mnemonic(phraseSkippingValidation: mnemonic))
}
static func words(matchingPrefix prefix: String) -> [String] {
let wordsList =
String(mcString: withMcInfallibleReturningOptional { mc_bip39_words_by_prefix(prefix) })
return wordsList.split(separator: ",").map { String($0) }
}
static func entropy(fromMnemonic mnemonic: Mnemonic) -> Data {
switch Data.make(withMcMutableBuffer: { bufferPtr, errorPtr in
mc_bip39_entropy_from_mnemonic(mnemonic.phrase, bufferPtr, &errorPtr)
}) {
case .success(let entropy):
return entropy
case .failure(let error):
switch error.errorCode {
case .invalidInput:
// Safety: mc_bip39_entropy_from_mnemonic should not return invalidInput as long as
// `mnemonic` is well-formed.
logger.fatalError(
"BIP39: error deriving entropy from mnemonic: \(redacting: error.description)")
default:
// Safety: mc_bip39_entropy_from_mnemonic should not throw non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: error)")
}
}
}
static func entropy(fromMnemonic mnemonic: String) -> Result<Data, InvalidInputError> {
Data.make(withMcMutableBuffer: { bufferPtr, errorPtr in
mc_bip39_entropy_from_mnemonic(mnemonic, bufferPtr, &errorPtr)
}).mapError {
switch $0.errorCode {
case .invalidInput:
return InvalidInputError(
"BIP39: error deriving entropy from mnemonic: \(redacting: $0.description)")
default:
// Safety: mc_bip39_entropy_from_mnemonic should not throw non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}
}

View File

@ -0,0 +1,28 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
public struct Mnemonic {
public static let allWords = Bip39Utils.words(matchingPrefix: "")
/// Entropy must be a multiple of 4 bytes and 16-32 bytes in length.
public static func mnemonic(fromEntropy entropy: Data) -> Result<String, InvalidInputError> {
Bip39Utils.mnemonic(fromEntropy: entropy).map { $0.phrase }
}
public static func entropy(fromMnemonic mnemonic: String) -> Result<Data, InvalidInputError> {
Bip39Utils.entropy(fromMnemonic: mnemonic)
}
public static func words(matchingPrefix prefix: String) -> [String] {
Bip39Utils.words(matchingPrefix: prefix)
}
let phrase: String
init(phraseSkippingValidation phrase: String) {
self.phrase = phrase
}
}

View File

@ -0,0 +1,85 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains operator_usage_whitespace
import Foundation
import LibMobileCoin
/// See https://github.com/satoshilabs/slips/blob/master/slip-0010.md
enum Slip10Utils {
static func accountPrivateKeys(fromMnemonic mnemonic: Mnemonic, accountIndex: UInt32)
-> (viewPrivateKey: RistrettoPrivate, spendPrivateKey: RistrettoPrivate)
{
var viewPrivateKeyOut = Data32()
var spendPrivateKeyOut = Data32()
viewPrivateKeyOut.asMcMutableBuffer { viewPrivateKeyOutPtr in
spendPrivateKeyOut.asMcMutableBuffer { spendPrivateKeyOutPtr in
switch withMcError({ errorPtr in
mc_slip10_account_private_keys_from_mnemonic(
mnemonic.phrase,
accountIndex,
viewPrivateKeyOutPtr,
spendPrivateKeyOutPtr,
&errorPtr)
}) {
case .success:
break
case .failure(let error):
switch error.errorCode {
case .invalidInput:
// Safety: mnemonic is guaranteed to satisfy
// mc_slip10_account_private_keys_from_mnemonic preconditions.
logger.fatalError(
"LibMobileCoin invalidInput error: \(redacting: error.description)")
default:
// Safety: mc_slip10_account_private_keys_from_mnemonic should not throw
// non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: error)")
}
}
}
}
// Safety: It's safe to skip validation because
// mc_slip10_account_private_keys_from_mnemonic should always return valid
// RistrettoPrivate values on success.
return (RistrettoPrivate(skippingValidation: viewPrivateKeyOut),
RistrettoPrivate(skippingValidation: spendPrivateKeyOut))
}
static func accountPrivateKeys(fromMnemonic mnemonic: String, accountIndex: UInt32)
-> Result<(viewPrivateKey: RistrettoPrivate, spendPrivateKey: RistrettoPrivate),
InvalidInputError>
{
var viewPrivateKeyOut = Data32()
var spendPrivateKeyOut = Data32()
return viewPrivateKeyOut.asMcMutableBuffer { viewPrivateKeyOutPtr in
spendPrivateKeyOut.asMcMutableBuffer { spendPrivateKeyOutPtr in
withMcError { errorPtr in
mc_slip10_account_private_keys_from_mnemonic(
mnemonic,
accountIndex,
viewPrivateKeyOutPtr,
spendPrivateKeyOutPtr,
&errorPtr)
}.mapError {
switch $0.errorCode {
case .invalidInput:
return InvalidInputError("\(redacting: $0.description)")
default:
// Safety: mc_slip10_account_private_keys_from_mnemonic should not throw
// non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}
}.map {
// Safety: It's safe to skip validation because
// mc_slip10_account_private_keys_from_mnemonic should always return valid
// RistrettoPrivate values on success.
return (RistrettoPrivate(skippingValidation: viewPrivateKeyOut),
RistrettoPrivate(skippingValidation: spendPrivateKeyOut))
}
}
}

View File

@ -0,0 +1,401 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable function_parameter_count multiline_arguments multiline_function_chains
import Foundation
import NIOSSL
public final class MobileCoinClient {
/// - Returns: `InvalidInputError` when `accountKey` isn't configured to use Fog.
public static func make(accountKey: AccountKey, config: Config)
-> Result<MobileCoinClient, InvalidInputError>
{
guard let accountKey = AccountKeyWithFog(accountKey: accountKey) else {
let errorMessage = "Accounts without fog URLs are not currently supported."
logger.error(errorMessage, logFunction: false)
return .failure(InvalidInputError(errorMessage))
}
return .success(MobileCoinClient(accountKey: accountKey, config: config))
}
private let accountLock: ReadWriteDispatchLock<Account>
private let serialQueue: DispatchQueue
private let callbackQueue: DispatchQueue
private let txOutSelectionStrategy: TxOutSelectionStrategy
private let mixinSelectionStrategy: MixinSelectionStrategy
private let fogQueryScalingStrategy: FogQueryScalingStrategy
private let serviceProvider: ServiceProvider
private let fogResolverManager: FogResolverManager
private let feeFetcher: BlockchainFeeFetcher
init(accountKey: AccountKeyWithFog, config: Config) {
logger.info("""
Initializing \(Self.self):
\(Self.configDescription(accountKey: accountKey, config: config))
""", logFunction: false)
self.serialQueue = DispatchQueue(label: "com.mobilecoin.\(Self.self)")
self.callbackQueue = config.callbackQueue ?? DispatchQueue.main
self.accountLock = .init(Account(accountKey: accountKey))
self.txOutSelectionStrategy = config.txOutSelectionStrategy
self.mixinSelectionStrategy = config.mixinSelectionStrategy
self.fogQueryScalingStrategy = config.fogQueryScalingStrategy
self.serviceProvider =
DefaultServiceProvider(networkConfig: config.networkConfig, targetQueue: serialQueue)
self.fogResolverManager = FogResolverManager(
fogReportAttestation: config.networkConfig.fogReportAttestation,
serviceProvider: serviceProvider,
targetQueue: serialQueue)
self.feeFetcher = BlockchainFeeFetcher(
blockchainService: serviceProvider.blockchainService,
minimumFeeCacheTTL: config.minimumFeeCacheTTL,
targetQueue: serialQueue)
}
public var balance: Balance {
accountLock.readSync { $0.cachedBalance }
}
public var accountActivity: AccountActivity {
accountLock.readSync { $0.cachedAccountActivity }
}
public func setTransportProtocol(_ transportProtocol: TransportProtocol) {
serviceProvider.setTransportProtocolOption(transportProtocol.option)
}
public func setConsensusBasicAuthorization(username: String, password: String) {
let credentials = BasicCredentials(username: username, password: password)
serviceProvider.setConsensusAuthorization(credentials: credentials)
}
public func setFogBasicAuthorization(username: String, password: String) {
let credentials = BasicCredentials(username: username, password: password)
serviceProvider.setFogUserAuthorization(credentials: credentials)
}
public func updateBalance(completion: @escaping (Result<Balance, ConnectionError>) -> Void) {
Account.BalanceUpdater(
account: accountLock,
fogViewService: serviceProvider.fogViewService,
fogKeyImageService: serviceProvider.fogKeyImageService,
fogBlockService: serviceProvider.fogBlockService,
fogQueryScalingStrategy: fogQueryScalingStrategy,
targetQueue: serialQueue
).updateBalance { result in
self.callbackQueue.async {
completion(result)
}
}
}
public func amountTransferable(
feeLevel: FeeLevel = .minimum,
completion: @escaping (Result<UInt64, BalanceTransferEstimationFetcherError>) -> Void
) {
Account.TransactionEstimator(
account: accountLock,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
targetQueue: serialQueue
).amountTransferable(feeLevel: feeLevel, completion: completion)
}
public func estimateTotalFee(
toSendAmount amount: UInt64,
feeLevel: FeeLevel = .minimum,
completion: @escaping (Result<UInt64, TransactionEstimationFetcherError>) -> Void
) {
Account.TransactionEstimator(
account: accountLock,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
targetQueue: serialQueue
).estimateTotalFee(toSendAmount: amount, feeLevel: feeLevel, completion: completion)
}
public func requiresDefragmentation(
toSendAmount amount: UInt64,
feeLevel: FeeLevel = .minimum,
completion: @escaping (Result<Bool, TransactionEstimationFetcherError>) -> Void
) {
Account.TransactionEstimator(
account: accountLock,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
targetQueue: serialQueue
).requiresDefragmentation(toSendAmount: amount, feeLevel: feeLevel, completion: completion)
}
public func prepareTransaction(
to recipient: PublicAddress,
amount: UInt64,
fee: UInt64,
completion: @escaping (
Result<(transaction: Transaction, receipt: Receipt), TransactionPreparationError>
) -> Void
) {
Account.TransactionOperations(
account: accountLock,
fogMerkleProofService: serviceProvider.fogMerkleProofService,
fogResolverManager: fogResolverManager,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
mixinSelectionStrategy: mixinSelectionStrategy,
targetQueue: serialQueue
).prepareTransaction(to: recipient, amount: amount, fee: fee) { result in
self.callbackQueue.async {
completion(result)
}
}
}
public func prepareTransaction(
to recipient: PublicAddress,
amount: UInt64,
feeLevel: FeeLevel = .minimum,
completion: @escaping (
Result<(transaction: Transaction, receipt: Receipt), TransactionPreparationError>
) -> Void
) {
Account.TransactionOperations(
account: accountLock,
fogMerkleProofService: serviceProvider.fogMerkleProofService,
fogResolverManager: fogResolverManager,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
mixinSelectionStrategy: mixinSelectionStrategy,
targetQueue: serialQueue
).prepareTransaction(to: recipient, amount: amount, feeLevel: feeLevel) { result in
self.callbackQueue.async {
completion(result)
}
}
}
public func prepareDefragmentationStepTransactions(
toSendAmount amount: UInt64,
feeLevel: FeeLevel = .minimum,
completion: @escaping (Result<[Transaction], DefragTransactionPreparationError>) -> Void
) {
Account.TransactionOperations(
account: accountLock,
fogMerkleProofService: serviceProvider.fogMerkleProofService,
fogResolverManager: fogResolverManager,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
mixinSelectionStrategy: mixinSelectionStrategy,
targetQueue: serialQueue
).prepareDefragmentationStepTransactions(toSendAmount: amount, feeLevel: feeLevel)
{ result in
self.callbackQueue.async {
completion(result)
}
}
}
public func submitTransaction(
_ transaction: Transaction,
completion: @escaping (Result<(), TransactionSubmissionError>) -> Void
) {
TransactionSubmitter(
consensusService: serviceProvider.consensusService,
feeFetcher: feeFetcher
).submitTransaction(transaction) { result in
self.callbackQueue.async {
completion(result)
}
}
}
public func status(
of transaction: Transaction,
completion: @escaping (Result<TransactionStatus, ConnectionError>) -> Void
) {
TransactionStatusChecker(
account: accountLock,
fogUntrustedTxOutService: serviceProvider.fogUntrustedTxOutService,
fogKeyImageService: serviceProvider.fogKeyImageService,
targetQueue: serialQueue
).checkStatus(transaction) { result in
self.callbackQueue.async {
completion(result)
}
}
}
public func status(of receipt: Receipt) -> Result<ReceiptStatus, InvalidInputError> {
ReceiptStatusChecker(account: accountLock).status(receipt)
}
}
extension MobileCoinClient {
private static func configDescription(accountKey: AccountKeyWithFog, config: Config) -> String {
let fogInfo = accountKey.fogInfo
return """
Consensus url: \(config.networkConfig.consensusUrl.url)
Fog url: \(config.networkConfig.fogUrl.url)
AccountKey PublicAddress: \
\(redacting: Base58Coder.encode(accountKey.accountKey.publicAddress))
AccountKey Fog Report url: \(fogInfo.reportUrl.url)
AccountKey Fog Report id: \(String(reflecting: fogInfo.reportId))
AccountKey Fog Report authority sPKI: 0x\(fogInfo.authoritySpki.hexEncodedString())
Consensus attestation: \(config.networkConfig.consensus.attestation)
Fog View attestation: \(config.networkConfig.fogView.attestation)
Fog KeyImage attestation: \(config.networkConfig.fogKeyImage.attestation)
Fog MerkleProof attestation: \(config.networkConfig.fogMerkleProof.attestation)
Fog Report attestation: \(config.networkConfig.fogReportAttestation)
"""
}
}
extension MobileCoinClient {
@available(*, deprecated, message: "Use amountTransferable(feeLevel:completion:) instead")
public func amountTransferable(feeLevel: FeeLevel = .minimum)
-> Result<UInt64, BalanceTransferEstimationError>
{
Account.TransactionEstimator(
account: accountLock,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
targetQueue: serialQueue
).amountTransferable(feeLevel: feeLevel)
}
@available(*, deprecated, message:
"Use estimateTotalFee(toSendAmount:feeLevel:completion:) instead")
public func estimateTotalFee(
toSendAmount amount: UInt64,
feeLevel: FeeLevel = .minimum
) -> Result<UInt64, TransactionEstimationError> {
Account.TransactionEstimator(
account: accountLock,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
targetQueue: serialQueue
).estimateTotalFee(toSendAmount: amount, feeLevel: feeLevel)
}
@available(*, deprecated, message:
"Use requiresDefragmentation(toSendAmount:feeLevel:completion:) instead")
public func requiresDefragmentation(toSendAmount amount: UInt64, feeLevel: FeeLevel = .minimum)
-> Result<Bool, TransactionEstimationError>
{
Account.TransactionEstimator(
account: accountLock,
feeFetcher: feeFetcher,
txOutSelectionStrategy: txOutSelectionStrategy,
targetQueue: serialQueue
).requiresDefragmentation(toSendAmount: amount, feeLevel: feeLevel)
}
}
extension MobileCoinClient {
public struct Config {
/// - Returns: `InvalidInputError` when `consensusUrl` or `fogUrl` are not well-formed URLs
/// with the appropriate schemes.
public static func make(
consensusUrl: String,
consensusAttestation: Attestation,
fogUrl: String,
fogViewAttestation: Attestation,
fogKeyImageAttestation: Attestation,
fogMerkleProofAttestation: Attestation,
fogReportAttestation: Attestation
) -> Result<Config, InvalidInputError> {
ConsensusUrl.make(string: consensusUrl).flatMap { consensusUrl in
FogUrl.make(string: fogUrl).map { fogUrl in
let attestationConfig = NetworkConfig.AttestationConfig(
consensus: consensusAttestation,
fogView: fogViewAttestation,
fogKeyImage: fogKeyImageAttestation,
fogMerkleProof: fogMerkleProofAttestation,
fogReport: fogReportAttestation)
let networkConfig = NetworkConfig(
consensusUrl: consensusUrl,
fogUrl: fogUrl,
attestation: attestationConfig)
return Config(networkConfig: networkConfig)
}
}
}
fileprivate var networkConfig: NetworkConfig
// default minimum fee cache TTL is 30 minutes
public var minimumFeeCacheTTL: TimeInterval = 30 * 60
public var cacheStorageAdapter: StorageAdapter?
/// The `DispatchQueue` on which all `MobileCoinClient` completion handlers will be called.
/// If `nil`, `DispatchQueue.main` will be used.
public var callbackQueue: DispatchQueue?
var txOutSelectionStrategy: TxOutSelectionStrategy = DefaultTxOutSelectionStrategy()
var mixinSelectionStrategy: MixinSelectionStrategy = DefaultMixinSelectionStrategy()
var fogQueryScalingStrategy: FogQueryScalingStrategy = DefaultFogQueryScalingStrategy()
init(networkConfig: NetworkConfig) {
self.networkConfig = networkConfig
}
public var transportProtocol: TransportProtocol {
get { networkConfig.transportProtocol }
set { networkConfig.transportProtocol = newValue }
}
public mutating func setConsensusTrustRoots(_ trustRoots: [Data])
-> Result<(), InvalidInputError>
{
Self.parseTrustRoots(trustRootsBytes: trustRoots).map {
networkConfig.consensusTrustRoots = $0
}
}
public mutating func setFogTrustRoots(_ trustRoots: [Data]) -> Result<(), InvalidInputError>
{
Self.parseTrustRoots(trustRootsBytes: trustRoots).map {
networkConfig.fogTrustRoots = $0
}
}
public mutating func setConsensusBasicAuthorization(username: String, password: String) {
networkConfig.consensusAuthorization =
BasicCredentials(username: username, password: password)
}
public mutating func setFogBasicAuthorization(username: String, password: String) {
networkConfig.fogUserAuthorization =
BasicCredentials(username: username, password: password)
}
public var httpRequester: HttpRequester? {
get { networkConfig.httpRequester }
set { networkConfig.httpRequester = newValue }
}
private static func parseTrustRoots(trustRootsBytes: [Data])
-> Result<[NIOSSLCertificate], InvalidInputError>
{
var trustRoots: [NIOSSLCertificate] = []
for trustRootBytes in trustRootsBytes {
do {
trustRoots.append(
try NIOSSLCertificate(bytes: Array(trustRootBytes), format: .der))
} catch {
let errorMessage = "Error parsing trust root certificate: " +
"\(trustRootBytes.base64EncodedString()) - Error: \(error)"
logger.error(errorMessage, logFunction: false)
return .failure(InvalidInputError(errorMessage))
}
}
return .success(trustRoots)
}
}
}

View File

@ -0,0 +1,274 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
import LibMobileCoin
enum AttestAkeError: Error {
case invalidInput(String)
case attestationVerificationFailed(String)
}
extension AttestAkeError: CustomStringConvertible {
var description: String {
"Attest Ake error: " + {
switch self {
case .invalidInput(let reason):
return "Invalid input: \(reason)"
case .attestationVerificationFailed(let reason):
return "Attestation verification failed: \(reason)"
}
}()
}
}
enum AeadError: Error {
case aead(String)
case cipher(String)
}
extension AeadError: CustomStringConvertible {
var description: String {
"Aead error: " + {
switch self {
case .aead(let reason):
return "Aead: \(reason)"
case .cipher(let reason):
return "Cipher: \(reason)"
}
}()
}
}
final class AttestAke {
private var state: State = .unattested
func authBeginRequest(
responderId: String,
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)? = nil,
rngContext: Any? = nil
) -> Attest_AuthMessage {
var request = Attest_AuthMessage()
request.data = authBeginRequestData(
responderId: responderId,
rng: rng,
rngContext: rngContext)
return request
}
func authBeginRequestData(
responderId: String,
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)? = nil,
rngContext: Any? = nil
) -> Data {
let ffi = FfiAttestAke()
let requestData = ffi.authBeginRequestData(
responderId: responderId,
rng: rng,
rngContext: rngContext)
state = .authPending(ffi)
return requestData
}
@discardableResult
func authEnd(authResponse: Attest_AuthMessage, attestationVerifier: AttestationVerifier)
-> Result<Cipher, AttestAkeError>
{
authEnd(authResponseData: authResponse.data, attestationVerifier: attestationVerifier)
}
@discardableResult
func authEnd(authResponseData: Data, attestationVerifier: AttestationVerifier)
-> Result<Cipher, AttestAkeError>
{
guard case .authPending(let attestAke) = state else {
return .failure(.invalidInput("AttestAke.authEnd called without a pending auth."))
}
return attestAke.authEnd(
authResponseData: authResponseData,
attestationVerifier: attestationVerifier
).map {
state = .attested(attestAke)
return Cipher(attestAke)
}
}
var isAttested: Bool {
if case .attested = state {
return true
} else {
return false
}
}
var cipher: Cipher? {
if case .attested(let attestAke) = state {
return Cipher(attestAke)
} else {
return nil
}
}
func deattest() {
state = .unattested
}
}
extension AttestAke {
struct Cipher {
private let ffi: FfiAttestAke
fileprivate init(_ ffi: FfiAttestAke) {
self.ffi = ffi
}
var binding: Data {
// Safety: ffi is guaranteed to be attested at this point, so ffi.binding() should
// never fail.
ffi.binding
}
func encryptMessage(aad: Data, plaintext: Data)
-> Result<Attest_Message, AeadError>
{
var message = Attest_Message()
message.aad = aad
message.channelID = binding
return encrypt(aad: aad, plaintext: plaintext).map {
message.data = $0
return message
}
}
func encrypt(aad: Data, plaintext: Data) -> Result<Data, AeadError> {
ffi.encrypt(aad: aad, plaintext: plaintext)
}
func decryptMessage(_ message: Attest_Message) -> Result<Data, AeadError> {
decrypt(aad: message.aad, ciphertext: message.data)
}
func decrypt(aad: Data, ciphertext: Data) -> Result<Data, AeadError> {
ffi.decrypt(aad: aad, ciphertext: ciphertext)
}
}
}
extension AttestAke {
private enum State {
case unattested
case authPending(FfiAttestAke)
case attested(FfiAttestAke)
}
}
private final class FfiAttestAke {
private let ptr: OpaquePointer
init() {
// Safety: mc_attest_ake_create should never return nil.
self.ptr = withMcInfallible(mc_attest_ake_create)
}
deinit {
mc_attest_ake_free(ptr)
}
var isAttested: Bool {
var attested = false
withMcInfallible {
mc_attest_ake_is_attested(ptr, &attested)
}
return attested
}
var binding: Data {
Data(withMcMutableBufferInfallible: { bufferPtr in
mc_attest_ake_get_binding(ptr, bufferPtr)
})
}
func authBeginRequestData(
responderId: String,
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)? = nil,
rngContext: Any? = nil
) -> Data {
withMcRngCallback(rng: rng, rngContext: rngContext) { rngCallbackPtr in
Data(withMcMutableBufferInfallible: { bufferPtr in
mc_attest_ake_get_auth_request(ptr, responderId, rngCallbackPtr, bufferPtr)
})
}
}
func authEnd(authResponseData: Data, attestationVerifier: AttestationVerifier)
-> Result<(), AttestAkeError>
{
authResponseData.asMcBuffer { bytesPtr in
attestationVerifier.withUnsafeOpaquePointer { attestationVerifierPtr in
withMcError { errorPtr in
mc_attest_ake_process_auth_response(
ptr,
bytesPtr,
attestationVerifierPtr,
&errorPtr)
}.mapError {
switch $0.errorCode {
case .invalidInput:
return .invalidInput("\(redacting: $0.description)")
case .attestationVerificationFailed:
return .attestationVerificationFailed("\(redacting: $0.description)")
default:
// Safety: mc_attest_ake_process_auth_response should not throw
// non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}
}
}
func encrypt(aad: Data, plaintext: Data) -> Result<Data, AeadError> {
aad.asMcBuffer { aadPtr in
plaintext.asMcBuffer { plaintextPtr in
Data.make(withMcMutableBuffer: { ciphertextOutPtr, errorPtr in
mc_attest_ake_encrypt(ptr, aadPtr, plaintextPtr, ciphertextOutPtr, &errorPtr)
}).mapError {
switch $0.errorCode {
case .aead:
return .aead("\(redacting: $0.description)")
case .cipher:
return .cipher("\(redacting: $0.description)")
default:
// Safety: mc_attest_ake_encrypt should not throw non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}
}
}
func decrypt(aad: Data, ciphertext: Data) -> Result<Data, AeadError> {
aad.asMcBuffer { aadPtr in
ciphertext.asMcBuffer { ciphertextPtr in
Data.make(withEstimatedLengthMcMutableBuffer: ciphertext.count)
{ plaintextOutPtr, errorPtr in
mc_attest_ake_decrypt(ptr, aadPtr, ciphertextPtr, plaintextOutPtr, &errorPtr)
}.mapError {
switch $0.errorCode {
case .aead:
return .aead("\(redacting: $0.description)")
case .cipher:
return .cipher("\(redacting: $0.description)")
default:
// Safety: mc_attest_ake_decrypt should not throw non-documented errors.
logger.fatalError("Unhandled LibMobileCoin error: \(redacting: $0)")
}
}
}
}
}
}

View File

@ -0,0 +1,187 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
// swiftlint:disable multiline_function_chains
import Foundation
public struct Attestation {
static func make(
mrSigner: Data,
productId: UInt16,
minimumSecurityVersion: UInt16,
allowedConfigAdvisories: [String] = [],
allowedHardeningAdvisories: [String] = []
) -> Result<Attestation, InvalidInputError> {
MrSigner.make(
mrSigner: mrSigner,
productId: productId,
minimumSecurityVersion: minimumSecurityVersion,
allowedConfigAdvisories: allowedConfigAdvisories,
allowedHardeningAdvisories: allowedHardeningAdvisories
).map { mrSigner in
Attestation(mrSigners: [mrSigner])
}
}
let mrEnclaves: [MrEnclave]
let mrSigners: [MrSigner]
public init(_ mrSigner: MrSigner) {
self.init(mrEnclaves: [], mrSigners: [mrSigner])
}
public init(mrEnclaves: [MrEnclave] = [], mrSigners: [MrSigner] = []) {
self.mrEnclaves = mrEnclaves
self.mrSigners = mrSigners
}
init(
mrSigner: Data32,
productId: UInt16,
minimumSecurityVersion: UInt16,
allowedConfigAdvisories: [String] = [],
allowedHardeningAdvisories: [String] = []
) {
let mrSigner = MrSigner(
mrSigner: mrSigner,
productId: productId,
minimumSecurityVersion: minimumSecurityVersion,
allowedConfigAdvisories: allowedConfigAdvisories,
allowedHardeningAdvisories: allowedHardeningAdvisories)
self.init(mrSigners: [mrSigner])
}
}
extension Attestation: CustomStringConvertible {
public var description: String {
var params: [String] = []
if mrEnclaves.count == 1 && mrSigners.isEmpty, let mrEnclave = mrEnclaves.first {
params.append("\(mrEnclave)")
} else if mrSigners.count == 1 && mrEnclaves.isEmpty, let mrSigner = mrSigners.first {
params.append("\(mrSigner)")
} else {
if !mrEnclaves.isEmpty {
params.append("mrEnclaves: \(mrEnclaves)")
}
if !mrSigners.isEmpty {
params.append("mrSigners: \(mrSigners)")
}
}
return "Attestation(\(params.joined(separator: ", ")))"
}
}
extension Attestation {
public struct MrEnclave {
let mrEnclave: Data32
let allowedConfigAdvisories: [String]
let allowedHardeningAdvisories: [String]
/// - Returns: `InvalidInputError` when `mrEnclave` is not 32 bytes in length.
public static func make(
mrEnclave: Data,
allowedConfigAdvisories: [String] = [],
allowedHardeningAdvisories: [String] = []
) -> Result<MrEnclave, InvalidInputError> {
guard let mrEnclave32 = Data32(mrEnclave) else {
let errorMessage = "mrEnclave must be 32 bytes in length. mrEnclave: " +
"\(mrEnclave.hexEncodedString())"
logger.error(errorMessage, logFunction: false)
return .failure(InvalidInputError(errorMessage))
}
return .success(MrEnclave(
mrEnclave: mrEnclave32,
allowedConfigAdvisories: allowedConfigAdvisories,
allowedHardeningAdvisories: allowedHardeningAdvisories))
}
init(
mrEnclave: Data32,
allowedConfigAdvisories: [String] = [],
allowedHardeningAdvisories: [String] = []
) {
self.mrEnclave = mrEnclave
self.allowedConfigAdvisories = allowedConfigAdvisories
self.allowedHardeningAdvisories = allowedHardeningAdvisories
}
}
}
extension Attestation.MrEnclave: CustomStringConvertible {
public var description: String {
var params = ["0x\(mrEnclave.hexEncodedString())"]
if !allowedConfigAdvisories.isEmpty {
params.append("allowedConfigAdvisories: \(allowedConfigAdvisories)")
}
if !allowedHardeningAdvisories.isEmpty {
params.append("allowedHardeningAdvisories: \(allowedHardeningAdvisories)")
}
return "MrEnclave(\(params.joined(separator: ", ")))"
}
}
extension Attestation {
public struct MrSigner {
let mrSigner: Data32
let productId: UInt16
let minimumSecurityVersion: UInt16
let allowedConfigAdvisories: [String]
let allowedHardeningAdvisories: [String]
/// - Returns: `InvalidInputError` when `mrSigner` is not 32 bytes in length.
public static func make(
mrSigner: Data,
productId: UInt16,
minimumSecurityVersion: UInt16,
allowedConfigAdvisories: [String] = [],
allowedHardeningAdvisories: [String] = []
) -> Result<MrSigner, InvalidInputError> {
guard let mrSigner32 = Data32(mrSigner) else {
let errorMessage =
"mrSigner must be 32 bytes in length. mrSigner: \(mrSigner.hexEncodedString())"
logger.error(errorMessage, logFunction: false)
return .failure(InvalidInputError(errorMessage))
}
return .success(MrSigner(
mrSigner: mrSigner32,
productId: productId,
minimumSecurityVersion: minimumSecurityVersion,
allowedConfigAdvisories: allowedConfigAdvisories,
allowedHardeningAdvisories: allowedHardeningAdvisories))
}
init(
mrSigner: Data32,
productId: UInt16,
minimumSecurityVersion: UInt16,
allowedConfigAdvisories: [String] = [],
allowedHardeningAdvisories: [String] = []
) {
self.mrSigner = mrSigner
self.productId = productId
self.minimumSecurityVersion = minimumSecurityVersion
self.allowedConfigAdvisories = allowedConfigAdvisories
self.allowedHardeningAdvisories = allowedHardeningAdvisories
}
}
}
extension Attestation.MrSigner: CustomStringConvertible {
public var description: String {
var params = [
"0x\(mrSigner.hexEncodedString())",
"productId: \(productId)",
"minimumSecurityVersion: \(minimumSecurityVersion)",
]
if !allowedConfigAdvisories.isEmpty {
params.append("allowedConfigAdvisories: \(allowedConfigAdvisories)")
}
if !allowedHardeningAdvisories.isEmpty {
params.append("allowedHardeningAdvisories: \(allowedHardeningAdvisories)")
}
return "MrSigner(\(params.joined(separator: ", ")))"
}
}

View File

@ -0,0 +1,119 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import LibMobileCoin
final class AttestationVerifier {
private let ptr: OpaquePointer
init(attestation: Attestation) {
// Safety: mc_verifier_create should never return nil.
self.ptr = withMcInfallible(mc_verifier_create)
attestation.mrEnclaves.forEach(addMrEnclave)
attestation.mrSigners.forEach(addMrSigner)
}
deinit {
mc_verifier_free(ptr)
}
func withUnsafeOpaquePointer<R>(_ body: (OpaquePointer) throws -> R) rethrows -> R {
try body(ptr)
}
private func addMrEnclave(_ mrEnclave: Attestation.MrEnclave) {
let ffiMrEnclaveVerifier = MrEnclaveVerifier(mrEnclave: mrEnclave)
ffiMrEnclaveVerifier.withUnsafeOpaquePointer { ffiMrEnclaveVerifierPtr in
// Safety: mc_verifier_add_mr_enclave should never fail.
withMcInfallible { mc_verifier_add_mr_enclave(ptr, ffiMrEnclaveVerifierPtr) }
}
}
private func addMrSigner(_ mrSigner: Attestation.MrSigner) {
let ffiMrSignerVerifier = MrSignerVerifier(mrSigner: mrSigner)
ffiMrSignerVerifier.withUnsafeOpaquePointer { ffiMrSignerVerifierPtr in
// Safety: mc_verifier_add_mr_signer should never fail.
withMcInfallible { mc_verifier_add_mr_signer(ptr, ffiMrSignerVerifierPtr) }
}
}
}
private final class MrEnclaveVerifier {
private let ptr: OpaquePointer
init(mrEnclave: Attestation.MrEnclave) {
self.ptr = mrEnclave.mrEnclave.asMcBuffer { mrEnclavePtr in
// Safety: mc_mr_enclave_verifier_create should never fail.
withMcInfallible { mc_mr_enclave_verifier_create(mrEnclavePtr) }
}
mrEnclave.allowedConfigAdvisories.forEach(addConfigAdvisory)
mrEnclave.allowedHardeningAdvisories.forEach(addHardeningAdvisory)
}
deinit {
mc_mr_enclave_verifier_free(ptr)
}
func withUnsafeOpaquePointer<R>(_ body: (OpaquePointer) throws -> R) rethrows -> R {
try body(ptr)
}
private func addConfigAdvisory(advisoryId: String) {
advisoryId.withCString { advisoryIdPtr in
// Safety: mc_mr_enclave_verifier_allow_config_advisory should never fail.
withMcInfallible { mc_mr_enclave_verifier_allow_config_advisory(ptr, advisoryIdPtr) }
}
}
private func addHardeningAdvisory(advisoryId: String) {
advisoryId.withCString { advisoryIdPtr in
// Safety: mc_mr_enclave_verifier_allow_hardening_advisory should never fail.
withMcInfallible { mc_mr_enclave_verifier_allow_hardening_advisory(ptr, advisoryIdPtr) }
}
}
}
private final class MrSignerVerifier {
private let ptr: OpaquePointer
init(mrSigner: Attestation.MrSigner) {
self.ptr = mrSigner.mrSigner.asMcBuffer { mrSignerPtr in
// Safety: mc_mr_signer_verifier_create should never fail.
withMcInfallible {
mc_mr_signer_verifier_create(
mrSignerPtr,
mrSigner.productId,
mrSigner.minimumSecurityVersion)
}
}
mrSigner.allowedConfigAdvisories.forEach(addConfigAdvisory)
mrSigner.allowedHardeningAdvisories.forEach(addHardeningAdvisory)
}
deinit {
mc_mr_signer_verifier_free(ptr)
}
func withUnsafeOpaquePointer<R>(_ body: (OpaquePointer) throws -> R) rethrows -> R {
try body(ptr)
}
private func addConfigAdvisory(advisoryId: String) {
advisoryId.withCString { advisoryIdPtr in
// Safety: mc_mr_signer_verifier_allow_config_advisory should never fail.
withMcInfallible { mc_mr_signer_verifier_allow_config_advisory(ptr, advisoryIdPtr) }
}
}
private func addHardeningAdvisory(advisoryId: String) {
advisoryId.withCString { advisoryIdPtr in
// Safety: mc_mr_signer_verifier_allow_hardening_advisory should never fail.
withMcInfallible { mc_mr_signer_verifier_allow_hardening_advisory(ptr, advisoryIdPtr) }
}
}
}

View File

@ -0,0 +1,34 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
import NIOSSL
protocol AttestedConnectionConfigProtocol: ConnectionConfigProtocol {
var attestation: Attestation { get }
}
struct AttestedConnectionConfig<Url: MobileCoinUrlProtocol>: AttestedConnectionConfigProtocol {
let urlTyped: Url
let transportProtocolOption: TransportProtocol.Option
let attestation: Attestation
let trustRoots: [NIOSSLCertificate]?
let authorization: BasicCredentials?
init(
url: Url,
transportProtocolOption: TransportProtocol.Option,
attestation: Attestation,
trustRoots: [NIOSSLCertificate]?,
authorization: BasicCredentials?
) {
self.urlTyped = url
self.transportProtocolOption = transportProtocolOption
self.attestation = attestation
self.trustRoots = trustRoots
self.authorization = authorization
}
var url: MobileCoinUrlProtocol { urlTyped }
}

View File

@ -0,0 +1,15 @@
//
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
//
import Foundation
struct BasicCredentials {
let username: String
let password: String
var authorizationHeaderValue: String {
let credentials = "\(username):\(password)"
return "Basic \(Data(credentials.utf8).base64EncodedString())"
}
}

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