Example #1
0
def delete_mutable(name, data_id, proxy=None, wallet_keys=None):
    """
    delete_mutable

    Remove a piece of mutable data from the user's profile. Delete it from
    the storage providers as well.

    Returns a dict with {'status': True} on success
    Returns a dict with {'error': ...} on failure
    """

    if proxy is None:
        proxy = get_default_proxy()
 
    fq_data_id = storage.make_fq_data_id( name, data_id )
    legacy = False
    user_profile, user_zonefile = get_name_profile( name, proxy=proxy, wallet_keys=wallet_keys, include_name_record=True )
    if user_profile is None:
        return user_zonefile    # will be an error message 

    name_record = user_zonefile['name_record']
    del user_zonefile['name_record']

    if blockstack_profiles.is_profile_in_legacy_format( user_zonefile ) or not user_db.is_user_zonefile( user_zonefile ):
        # zonefile is a legacy profile.  There is no immutable data 
        log.info("Profile is in legacy format.  No immutable data.")
        return {'status': True}

    # already deleted?
    if not user_db.has_mutable_data( user_profile, data_id ):
        return {'status': True}

    # unlink
    user_db.remove_mutable_data_zonefile( user_profile, data_id )

    # put new profile 
    data_privkey = get_data_or_owner_privkey( user_zonefile, name_record['address'], wallet_keys=wallet_keys, config_path=proxy.conf['path'] )
    if 'error' in data_privkey:
        return {'error': data_privkey['error']}
    else:
        data_privkey = data_privkey['privatekey']
        assert data_privkey is not None

    rc = storage.put_mutable_data( name, user_profile, data_privkey )
    if not rc:
        return {'error': 'Failed to unlink mutable data from profile'}

    # remove the data itself 
    rc = storage.delete_mutable_data( fq_data_id, data_privkey )
    if not rc:
        return {'error': 'Failed to delete mutable data from storage providers'}

    return {'status': True}
Example #2
0
def app_delete_resource(blockchain_id,
                        app_domain,
                        res_name,
                        app_config=None,
                        data_privkey=None,
                        proxy=None,
                        wallet_keys=None,
                        config_path=CONFIG_PATH):
    """
    Remove data from a named application resource in mutable storage.

    data_privkey should be the publisher's private key
    name should be a blockchain ID that points to the public key

    if app_config is not None, then the driver hints will be honored.

    Return {'status': True, 'version': ...} on success
    Return {'error': ...} on error
    """

    if data_privkey is None:
        assert wallet_keys, "No data private key or wallet given"
        data_privkey = wallet_keys.get('data_privkey', None)
        assert data_privkey, "Wallet does not contain a data private key"

    data_pubkey = get_pubkey_hex(data_privkey)

    proxy = get_default_proxy() if proxy is None else proxy

    res_data_id = storage.make_fq_data_id(app_domain, res_name)

    driver_hints = None
    if app_config is not None:
        # use driver hints
        driver_hints = app_config['driver_hints']

    tombstone = storage.make_data_tombstone(res_data_id)
    signed_tombstone = storage.sign_data_tombstone(res_data_id, data_privkey)
    res = data.delete_mutable(res_data_id, [signed_tombstone],
                              proxy=proxy,
                              storage_drivers=driver_hints,
                              blockchain_id=blockchain_id,
                              is_fq_data_id=True,
                              config_path=config_path)
    if 'error' in res:
        log.error("Failed to delete resource {}: {}".format(
            res_data_id, res['error']))
        return {'error': 'Failed to delete resource'}

    return {'status': True}
def app_put_resource( blockchain_id, app_domain, res_name, res_data, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ):
    """
    Store data to a named application resource in mutable storage.

    data_privkey should be the publisher's private key
    name should be a blockchain ID that points to the public key

    if app_config is not None, then the driver hints will be honored.

    Return {'status': True, 'version': ...} on success
    Return {'error': ...} on error
    """

    assert isinstance(res_data, (str, unicode)), "Resource must be a string"
    try:
        json.dumps(res_data)
    except:
        raise AssertionError("Resource must be a JSON-serializable string")

    if data_privkey is None:
        assert wallet_keys, 'Missing both data private key and wallet keys'
        data_privkey = wallet_keys.get('data_privkey')
        assert data_privkey, "Wallet does not have a data private key"

    proxy = get_default_proxy() if proxy is None else proxy

    res_data_id = storage.make_fq_data_id(app_domain, res_name)
    data_pubkey = get_pubkey_hex(data_privkey)

    driver_hints = None
    if app_config is not None:
        # use driver hints
        driver_hints = app_config['driver_hints']

    res_blob = data.make_mutable_data_info(res_data_id, res_data, is_fq_data_id=True)
    res_blob_str = data.data_blob_serialize(res_blob)
    res_sig = data.data_blob_sign(res_blob_str, data_privkey)
    res = data.put_mutable(res_data_id, res_blob_str, data_pubkey, res_sig, res_blob['version'], blockchain_id=blockchain_id, config_path=config_path, storage_drivers=driver_hints)
    if 'error' in res:
        log.error("Failed to store resource {}: {}".format(res_data_id, res['error']))
        return {'error': 'Failed to store resource'}

    return {'status': True, 'version': res_blob['version']}
def app_delete_resource( blockchain_id, app_domain, res_name, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ):
    """
    Remove data from a named application resource in mutable storage.

    data_privkey should be the publisher's private key
    name should be a blockchain ID that points to the public key

    if app_config is not None, then the driver hints will be honored.

    Return {'status': True, 'version': ...} on success
    Return {'error': ...} on error
    """

    if data_privkey is None:
        assert wallet_keys, "No data private key or wallet given"
        data_privkey = wallet_keys.get('data_privkey', None)
        assert data_privkey, "Wallet does not contain a data private key"

    data_pubkey = get_pubkey_hex(data_privkey)

    proxy = get_default_proxy() if proxy is None else proxy

    res_data_id = storage.make_fq_data_id(app_domain, res_name)

    driver_hints = None
    if app_config is not None:
        # use driver hints
        driver_hints = app_config['driver_hints']

    tombstone = storage.make_data_tombstone(res_data_id)
    signed_tombstone = storage.sign_data_tombstone(res_data_id, data_privkey)
    res = data.delete_mutable(res_data_id, [signed_tombstone], proxy=proxy, storage_drivers=driver_hints, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path)
    if 'error' in res:
        log.error("Failed to delete resource {}: {}".format(res_data_id, res['error']))
        return {'error': 'Failed to delete resource'}

    return {'status': True}
Example #5
0
def put_mutable(name, data_id, data_json, proxy=None, create_only=False, update_only=False, 
                txid=None, version=None, make_version=None, wallet_keys=None):
    """
    put_mutable

    Given a name, an ID for the data, and the data itself, sign and upload the data to the
    configured storage providers.  Add an entry for it into the user's profile as well.

    ** Consistency **

    @version, if given, is the version to include in the data.
    @make_version, if given, is a callback that takes the data_id, data_json, and current version as arguments, and generates the version to be included in the data record uploaded.
    If ver is not given, but make_ver is, then make_ver will be used to generate the version.
    If neither ver nor make_ver are given, the mutable data (if it already exists) is fetched, and the version is calculated as the larget known version + 1.

    ** Durability **

    Replication is best-effort.  If one storage provider driver succeeds, the put_mutable succeeds.  If they all fail, then put_mutable fails.
    More complex behavior can be had by creating a "meta-driver" that calls existing drivers' methods in the desired manner.

    Returns a dict with {'status': True, 'version': version, ...} on success
    Returns a dict with 'error' set on failure
    """

    if type(data_json) not in [dict]:
        raise ValueError("Mutable data must be a dict")

    if proxy is None:
        proxy = get_default_proxy()

    fq_data_id = storage.make_fq_data_id( name, data_id )

    name_record = None
    user_profile, user_zonefile, created_new_zonefile = get_and_migrate_profile( name, create_if_absent=True, proxy=proxy, wallet_keys=wallet_keys, include_name_record=True )
    if 'error' in user_profile:
        return user_profile 

    if created_new_zonefile:
        log.debug("User profile is legacy")
        return {'error': "User profile is in legacy format, which does not support this operation.  You must first migrate it with the 'migrate' command."}

    name_record = user_zonefile['name_record']
    del user_zonefile['name_record']

    log.debug("Profile for %s is currently:\n%s" % (name, json.dumps(user_profile, indent=4, sort_keys=True)))

    exists = user_db.has_mutable_data( user_profile, data_id )
    if not exists and update_only:
        return {'error': 'Mutable datum does not exist'}

    if exists and create_only:
        return {'error': 'Mutable datum already exists'}
    
    # get the version to use
    if version is None:
        version = put_mutable_get_version( user_profile, data_id, data_json, make_version=make_version )

    # generate the mutable zonefile
    data_privkey = get_data_or_owner_privkey( user_zonefile, name_record['address'], wallet_keys=wallet_keys, config_path=proxy.conf['path'] )
    if 'error' in data_privkey:
        # error text
        return {'error': data_privkey['error']}

    else:
        data_privkey = data_privkey['privatekey']
        assert data_privkey is not None

    urls = storage.make_mutable_data_urls( fq_data_id )
    mutable_zonefile = user_db.make_mutable_data_zonefile( data_id, version, urls )

    # add the mutable zonefile to the profile
    rc = user_db.put_mutable_data_zonefile( user_profile, data_id, version, mutable_zonefile )
    assert rc, "Failed to put mutable data zonefile"

    # for legacy migration...
    txid = None 
    zonefile_hash = None
    result = {}
 
    # update the profile with the new zonefile
    rc = storage.put_mutable_data( name, user_profile, data_privkey )
    if not rc:
        result['error'] = 'Failed to store mutable data zonefile to profile'
        return result

    # put the mutable data record itself
    rc = storage.put_mutable_data( fq_data_id, data_json, data_privkey )
    if not rc:
        result['error'] = "Failed to store mutable data"
        return result

    # remember which version this was 
    rc = store_mutable_data_version(proxy.conf, fq_data_id, version)
    if not rc:
        result['error'] = "Failed to store mutable data version"
        return result

    result['status'] = True
    result['version'] = version
    log.debug("Put '%s' to %s mutable data (version %s)\nProfile is now:\n%s" % (data_id, name, version, json.dumps(user_profile, indent=4, sort_keys=True)))
    return result
Example #6
0
def get_mutable(name, data_id, proxy=None, ver_min=None, ver_max=None, ver_check=None, conf=None, wallet_keys=None):
    """
    get_mutable

    Fetch a piece of mutable data.  Use @data_id to look it up in the user's
    profile, and then fetch and erify the data itself from the configured 
    storage providers.

    If @ver_min is given, ensure the data's version is greater or equal to it.
    If @ver_max is given, ensure the data's version is less than it.
    If @ver_check is given, it must be a callable that takes the name, data and version and returns True/False

    Return {'data': the data, 'version': the version} on success
    Return {'error': ...} on error
    """

    if proxy is None:
        proxy = get_default_proxy()

    if conf is None:
        conf = proxy.conf

    fq_data_id = storage.make_fq_data_id( name, data_id )
    user_profile, user_zonefile = get_name_profile( name, proxy=proxy, wallet_keys=wallet_keys, include_name_record=True )
    if user_profile is None:
        return user_zonefile    # will be an error message
   
    # recover name record 
    name_record = user_zonefile['name_record']
    del user_zonefile['name_record']

    if blockstack_profiles.is_profile_in_legacy_format( user_zonefile ) or not user_db.is_user_zonefile( user_zonefile ):
        # profile has not been converted to the new zonefile format yet.
        return {'error': 'Profile is in a legacy format that does not support mutable data.'}

    # get the mutable data zonefile
    if not user_db.has_mutable_data( user_profile, data_id ):
        return {'error': "No such mutable datum"}

    mutable_data_zonefile = user_db.get_mutable_data_zonefile( user_profile, data_id )
    assert mutable_data_zonefile is not None, "BUG: could not look up mutable datum '%s'.'%s'" % (name, data_id)

    # get user's data public key and owner address
    data_pubkey = user_db.user_zonefile_data_pubkey( user_zonefile )
    data_address = name_record['address']
    if data_pubkey is None:
        log.warn("Falling back to owner address for authentication")

    # get the mutable data itself
    urls = user_db.mutable_data_zonefile_urls( mutable_data_zonefile )
    mutable_data = storage.get_mutable_data(fq_data_id, data_pubkey, urls=urls, data_address=data_address )
    if mutable_data is None:
        return {'error': "Failed to look up mutable datum"}

    expected_version = load_mutable_data_version( conf, name, data_id )
    if expected_version is None:
        expected_version = 0

    # check consistency
    version = user_db.mutable_data_version( user_profile, data_id )
    if ver_min is not None and ver_min > version:
        return {'error': 'Mutable data is stale'}

    if ver_max is not None and ver_max <= version:
        return {'error': 'Mutable data is in the future'}

    if ver_check is not None:
        rc = ver_check( name, mutable_data, version )
        if not rc:
            return {'error': 'Mutable data consistency check failed'}

    elif expected_version > version:
        return {'error': 'Mutable data is stale; a later version was previously fetched'}

    rc = store_mutable_data_version( conf, fq_data_id, version )
    if not rc:
        return {'error': 'Failed to store consistency information'}

    return {'data': mutable_data, 'version': version}
def app_unpublish( blockchain_id, app_domain, force=False, data_privkey=None, app_config=None, wallet_keys=None, proxy=None, config_path=CONFIG_PATH ):
    """
    Unpublish an application
    Deletes its config and index.
    Does NOT delete its resources.
    Does NOT delete user data.

    if force is True, then we will try to delete the app state even if we can't load the app config
    WARNING: force can be dangerous, since it can delete data via drivers that were never meant for this app.  Use with caution!

    Return {'status': True, 'app_config': ..., 'retry': ...} on success.  If retry is True, then retry this method with the given app_config
    Return {'error': ...} on error
    """

    proxy = get_default_proxy() if proxy is None else proxy

    # find out where to delete from
    data_pubkey = None
    if data_privkey is not None:
        data_pubkey = get_pubkey_hex(str(data_privkey))

    if app_config is None:
        app_config = app_get_config(blockchain_id, app_domain, data_pubkey=data_pubkey, proxy=proxy, config_path=CONFIG_PATH )
        if 'error' in app_config:
            if not force:
                log.error("Failed to load app config for {}'s {}".format(blockchain_id, app_domain))
                return {'error': 'Failed to load app config'}
            else:
                # keep going 
                app_config = None
                log.warning("Failed to load app config, but proceeding at caller request")

    config_data_id = storage.make_fq_data_id(app_domain, '.blockstack')
    index_data_id = storage.make_fq_data_id(app_domain, 'index.html')

    storage_drivers = None
    if app_config is not None:
        # only use the ones we have to 
        urls = user_db.urls_from_uris(app_config['index_uris'])
        driver_names = []

        for url in urls:
            drivers = storage.get_drivers_for_url(url)
            driver_names += [d.__name__ for d in drivers]

        storage_drivers = list(set(driver_names))
    
    ret = {}

    # delete the index
    index_tombstone = storage.make_data_tombstone(index_data_id)
    signed_index_tombstone = storage.sign_data_tombstone(index_data_id, data_privkey)
    res = data.delete_mutable(index_data_id, [signed_index_tombstone], proxy=proxy, storage_drivers=storage_drivers, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path)
    if 'error' in res:
        log.warning("Failed to delete index file {}".format(index_data_id))
        ret['app_config'] = app_config
        ret['retry'] = True

    # delete the config 
    config_tombstone = storage.make_data_tombstone(config_data_id)
    signed_config_tombstone = storage.sign_data_tombstone(config_data_id, data_privkey)
    res = data.delete_mutable(config_data_id, [signed_config_tombstone], proxy=proxy, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path)
    if 'error' in res:
        log.warning("Failed to delete config file {}".format(config_data_id))
        if not ret.has_key('app_config'):
            ret['app_config'] = app_config

        ret['retry'] = True

    ret['status'] = True
    return ret
def app_publish( dev_blockchain_id, app_domain, app_method_list, app_index_uris, app_index_file, app_driver_hints=[], data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ):
    """
    Instantiate an application.
    * replicate the (opaque) app index file to "index.html" to each URL in app_uris
    * replicate the list of URIs and the list of methods to ".blockstack" via each of the client's storage drivers.

    This succeeds even if the app already exists (in which case,
    it will be overwritten).  This method is idempotent, so it
    can be retried on failure.

    data_privkey should be the publisher's private key (i.e. their data key)
    name should be the blockchain ID that points to data_pubkey
   
    Return {'status': True, 'config_fq_data_id': config's fully-qualified data ID, 'index_fq_data_id': index file's fully-qualified data ID} on success
    Return {'error': ...} on error
    """

    if data_privkey is None:
        assert wallet_keys, 'Missing both data private key and wallet keys'
        data_privkey = wallet_keys.get('data_privkey')
        assert data_privkey, "Wallet does not have a data private key"

    proxy = get_default_proxy() if proxy is None else proxy

    # replicate configuration data (method list and app URIs)
    app_cfg = {
        'blockchain_id': dev_blockchain_id,
        'app_domain': app_domain,
        'index_uris': app_index_uris,
        'api_methods': app_method_list,
        'driver_hints': app_driver_hints,
    }

    jsonschema.validate(app_cfg, APP_CONFIG_SCHEMA)
    
    data_pubkey = get_pubkey_hex(data_privkey)
    config_data_id = storage.make_fq_data_id(app_domain, '.blockstack')

    app_cfg_blob = data.make_mutable_data_info(config_data_id, app_cfg, is_fq_data_id=True)
    app_cfg_str = data.data_blob_serialize(app_cfg_blob)
    app_cfg_sig = data.data_blob_sign( app_cfg_str, data_privkey )
    res = data.put_mutable(config_data_id, app_cfg_str, data_pubkey, app_cfg_sig, app_cfg_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path)
    if 'error' in res:
        log.error('Failed to replicate application configuration {}: {}'.format(config_data_id, res['error']))
        return {'error': 'Failed to replicate application config'}

    # what drivers to use for the index file?
    urls = user_db.urls_from_uris(app_index_uris)
    driver_names = []

    for url in urls:
        drivers = storage.get_drivers_for_url(url)
        driver_names += [d.__name__ for d in drivers]

    driver_names = list(set(driver_names))
    index_data_id = storage.make_fq_data_id(app_domain, 'index.html')
    
    # replicate app index file (at least one must succeed)
    # NOTE: the publisher is free to use alternative URIs that are not supported; they'll just be ignored.
    app_index_blob = data.make_mutable_data_info(index_data_id, app_index_file, is_fq_data_id=True)
    app_index_blob_str = data.data_blob_serialize(app_index_blob)
    app_index_sig = data.data_blob_sign(app_index_blob_str, data_privkey)
    res = data.put_mutable( index_data_id, app_index_blob_str, data_pubkey, app_index_sig, app_index_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path, storage_drivers=app_driver_hints )
    if 'error' in res:
        log.error("Failed to replicate application index file to {}: {}".format(",".join(urls), res['error']))
        return {'error': 'Failed to replicate index file'}

    return {'status': True, 'config_fq_data_id': config_data_id, 'index_fq_data_id': index_data_id}
Example #9
0
def app_unpublish(blockchain_id,
                  app_domain,
                  force=False,
                  data_privkey=None,
                  app_config=None,
                  wallet_keys=None,
                  proxy=None,
                  config_path=CONFIG_PATH):
    """
    Unpublish an application
    Deletes its config and index.
    Does NOT delete its resources.
    Does NOT delete user data.

    if force is True, then we will try to delete the app state even if we can't load the app config
    WARNING: force can be dangerous, since it can delete data via drivers that were never meant for this app.  Use with caution!

    Return {'status': True, 'app_config': ..., 'retry': ...} on success.  If retry is True, then retry this method with the given app_config
    Return {'error': ...} on error
    """

    proxy = get_default_proxy() if proxy is None else proxy

    # find out where to delete from
    data_pubkey = None
    if data_privkey is not None:
        data_pubkey = get_pubkey_hex(str(data_privkey))

    if app_config is None:
        app_config = app_get_config(blockchain_id,
                                    app_domain,
                                    data_pubkey=data_pubkey,
                                    proxy=proxy,
                                    config_path=CONFIG_PATH)
        if 'error' in app_config:
            if not force:
                log.error("Failed to load app config for {}'s {}".format(
                    blockchain_id, app_domain))
                return {'error': 'Failed to load app config'}
            else:
                # keep going
                app_config = None
                log.warning(
                    "Failed to load app config, but proceeding at caller request"
                )

    config_data_id = storage.make_fq_data_id(app_domain, '.blockstack')
    index_data_id = storage.make_fq_data_id(app_domain, 'index.html')

    storage_drivers = None
    if app_config is not None:
        # only use the ones we have to
        urls = user_db.urls_from_uris(app_config['index_uris'])
        driver_names = []

        for url in urls:
            drivers = storage.get_drivers_for_url(url)
            driver_names += [d.__name__ for d in drivers]

        storage_drivers = list(set(driver_names))

    ret = {}

    # delete the index
    index_tombstone = storage.make_data_tombstone(index_data_id)
    signed_index_tombstone = storage.sign_data_tombstone(
        index_data_id, data_privkey)
    res = data.delete_mutable(index_data_id, [signed_index_tombstone],
                              proxy=proxy,
                              storage_drivers=storage_drivers,
                              blockchain_id=blockchain_id,
                              is_fq_data_id=True,
                              config_path=config_path)
    if 'error' in res:
        log.warning("Failed to delete index file {}".format(index_data_id))
        ret['app_config'] = app_config
        ret['retry'] = True

    # delete the config
    config_tombstone = storage.make_data_tombstone(config_data_id)
    signed_config_tombstone = storage.sign_data_tombstone(
        config_data_id, data_privkey)
    res = data.delete_mutable(config_data_id, [signed_config_tombstone],
                              proxy=proxy,
                              blockchain_id=blockchain_id,
                              is_fq_data_id=True,
                              config_path=config_path)
    if 'error' in res:
        log.warning("Failed to delete config file {}".format(config_data_id))
        if not ret.has_key('app_config'):
            ret['app_config'] = app_config

        ret['retry'] = True

    ret['status'] = True
    return ret
Example #10
0
def app_put_resource(blockchain_id,
                     app_domain,
                     res_name,
                     res_data,
                     app_config=None,
                     data_privkey=None,
                     proxy=None,
                     wallet_keys=None,
                     config_path=CONFIG_PATH):
    """
    Store data to a named application resource in mutable storage.

    data_privkey should be the publisher's private key
    name should be a blockchain ID that points to the public key

    if app_config is not None, then the driver hints will be honored.

    Return {'status': True, 'version': ...} on success
    Return {'error': ...} on error
    """

    assert isinstance(res_data, (str, unicode)), "Resource must be a string"
    try:
        json.dumps(res_data)
    except:
        raise AssertionError("Resource must be a JSON-serializable string")

    if data_privkey is None:
        assert wallet_keys, 'Missing both data private key and wallet keys'
        data_privkey = wallet_keys.get('data_privkey')
        assert data_privkey, "Wallet does not have a data private key"

    proxy = get_default_proxy() if proxy is None else proxy

    res_data_id = storage.make_fq_data_id(app_domain, res_name)
    data_pubkey = get_pubkey_hex(data_privkey)

    driver_hints = None
    if app_config is not None:
        # use driver hints
        driver_hints = app_config['driver_hints']

    res_blob = data.make_mutable_data_info(res_data_id,
                                           res_data,
                                           is_fq_data_id=True)
    res_blob_str = data.data_blob_serialize(res_blob)
    res_sig = data.data_blob_sign(res_blob_str, data_privkey)
    res = data.put_mutable(res_data_id,
                           res_blob_str,
                           data_pubkey,
                           res_sig,
                           res_blob['version'],
                           blockchain_id=blockchain_id,
                           config_path=config_path,
                           storage_drivers=driver_hints)
    if 'error' in res:
        log.error("Failed to store resource {}: {}".format(
            res_data_id, res['error']))
        return {'error': 'Failed to store resource'}

    return {'status': True, 'version': res_blob['version']}
Example #11
0
def app_publish(dev_blockchain_id,
                app_domain,
                app_method_list,
                app_index_uris,
                app_index_file,
                app_driver_hints=[],
                data_privkey=None,
                proxy=None,
                wallet_keys=None,
                config_path=CONFIG_PATH):
    """
    Instantiate an application.
    * replicate the (opaque) app index file to "index.html" to each URL in app_uris
    * replicate the list of URIs and the list of methods to ".blockstack" via each of the client's storage drivers.

    This succeeds even if the app already exists (in which case,
    it will be overwritten).  This method is idempotent, so it
    can be retried on failure.

    data_privkey should be the publisher's private key (i.e. their data key)
    name should be the blockchain ID that points to data_pubkey
   
    Return {'status': True, 'config_fq_data_id': config's fully-qualified data ID, 'index_fq_data_id': index file's fully-qualified data ID} on success
    Return {'error': ...} on error
    """

    if data_privkey is None:
        assert wallet_keys, 'Missing both data private key and wallet keys'
        data_privkey = wallet_keys.get('data_privkey')
        assert data_privkey, "Wallet does not have a data private key"

    proxy = get_default_proxy() if proxy is None else proxy

    # replicate configuration data (method list and app URIs)
    app_cfg = {
        'blockchain_id': dev_blockchain_id,
        'app_domain': app_domain,
        'index_uris': app_index_uris,
        'api_methods': app_method_list,
        'driver_hints': app_driver_hints,
    }

    jsonschema.validate(app_cfg, APP_CONFIG_SCHEMA)

    data_pubkey = get_pubkey_hex(data_privkey)
    config_data_id = storage.make_fq_data_id(app_domain, '.blockstack')

    app_cfg_blob = data.make_mutable_data_info(config_data_id,
                                               app_cfg,
                                               is_fq_data_id=True)
    app_cfg_str = data.data_blob_serialize(app_cfg_blob)
    app_cfg_sig = data.data_blob_sign(app_cfg_str, data_privkey)
    res = data.put_mutable(config_data_id,
                           app_cfg_str,
                           data_pubkey,
                           app_cfg_sig,
                           app_cfg_blob['version'],
                           blockchain_id=dev_blockchain_id,
                           config_path=config_path)
    if 'error' in res:
        log.error(
            'Failed to replicate application configuration {}: {}'.format(
                config_data_id, res['error']))
        return {'error': 'Failed to replicate application config'}

    # what drivers to use for the index file?
    urls = user_db.urls_from_uris(app_index_uris)
    driver_names = []

    for url in urls:
        drivers = storage.get_drivers_for_url(url)
        driver_names += [d.__name__ for d in drivers]

    driver_names = list(set(driver_names))
    index_data_id = storage.make_fq_data_id(app_domain, 'index.html')

    # replicate app index file (at least one must succeed)
    # NOTE: the publisher is free to use alternative URIs that are not supported; they'll just be ignored.
    app_index_blob = data.make_mutable_data_info(index_data_id,
                                                 app_index_file,
                                                 is_fq_data_id=True)
    app_index_blob_str = data.data_blob_serialize(app_index_blob)
    app_index_sig = data.data_blob_sign(app_index_blob_str, data_privkey)
    res = data.put_mutable(index_data_id,
                           app_index_blob_str,
                           data_pubkey,
                           app_index_sig,
                           app_index_blob['version'],
                           blockchain_id=dev_blockchain_id,
                           config_path=config_path,
                           storage_drivers=app_driver_hints)
    if 'error' in res:
        log.error(
            "Failed to replicate application index file to {}: {}".format(
                ",".join(urls), res['error']))
        return {'error': 'Failed to replicate index file'}

    return {
        'status': True,
        'config_fq_data_id': config_data_id,
        'index_fq_data_id': index_data_id
    }